/**
 * SQLite-backed session index and full-text search.
 *
 * Stores session metadata and user-prompt text in a WAL-mode SQLite database
 * at ~/.agents/.history/sessions/sessions.db. Provides incremental upsert, scan-stamp
 * ledger (mtime/size tracking to skip unchanged files), FTS5 search with
 * BM25 ranking, and label-first search for /rename'd sessions.
 */
import Database from '../sqlite.js';
import type { SessionAgentId, SessionCheckpoint, SessionChecklistItem, SessionEvent, SessionMeta, SummaryState } from './types.js';
import { type IndexedToolCall } from './tool-calls.js';
import { type ToolScanResumePoint } from './tool-store.js';
/** Current schema version; bumped when migrations are added. Exported so tests
 * assert against the constant instead of hardcoding a number that every bump
 * then has to chase (docs/sessions.md calls the constant the source of truth). */
export declare const SCHEMA_VERSION = 47;
/**
 * Bump to force the content extractor (assistant-answer text, alongside the
 * user-prompt text every harness already accumulates) to re-derive on every
 * session's next scan. Unlike RESOURCE_INDEX_VERSION this is read by the
 * change-detector itself (`filterChangedEntries`, discover.ts) via
 * `scan_ledger.extractor_version` — a stored version below this one is treated
 * as "changed" even when the file's (mtime, size) are unchanged, so bumping it
 * here backfills every existing session's assistant text on its next scan
 * without a `DELETE FROM scan_ledger` (which would also throw away the
 * resumable parser_state/content_text for Claude/Codex).
 *
 * v3 (PHNX-3621) invalidates OpenCode rows that merged v2 stamped before its
 * genuine first-user-turn extractor was added.
 * v4 (PHNX-3621 leftover) invalidates Grok rows stamped before the bounded
 * chat_history.jsonl prefix read filled firstUserMessage.
 */
export declare const CONTENT_INDEX_VERSION = 4;
/**
 * Bumping this invalidates every cached facet row without touching the schema
 * version, so a change to the extraction logic (a new metric, a corrected bucket)
 * re-derives on the next `agents insights` instead of silently reporting stale
 * numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
 */
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
/** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
/** Bump when the summarizer output shape changes so cached summaries recompute (PHNX-3939 v1). */
export declare const SESSION_SUMMARY_EXTRACTOR_VERSION = 1;
/** File stat snapshot used to detect changes between scan runs. */
export interface ScanStamp {
    fileMtimeMs: number;
    fileSize: number;
    scannedAt?: number;
    /**
     * `scan_ledger.extractor_version` as of the last scan, when read from the
     * ledger (undefined for a freshly-computed stamp that hasn't been persisted
     * yet). Compared against {@link CONTENT_INDEX_VERSION} by
     * `filterChangedEntries` (discover.ts) to force a re-extract independent of
     * (mtime, size).
     */
    extractorVersion?: number | null;
}
/** Filter and pagination options for querying the sessions table. */
export interface QueryOptions {
    agent?: SessionAgentId;
    agents?: SessionAgentId[];
    origin?: 'cli' | 'routine';
    version?: string;
    cwd?: string;
    /** Match any session whose cwd equals this or is a descendant of it. */
    cwdPrefix?: string;
    project?: string;
    /** Only sessions recorded on this machine (host), case-insensitive. */
    machine?: string;
    /** Match the full session id or short id, case-insensitively (exact). */
    idExact?: string;
    /** Match sessions whose id or short id begins with this (case-insensitive prefix). */
    idPrefix?: string;
    sinceMs?: number;
    untilMs?: number;
    limit?: number;
    /** Drop rows flagged as team-origin before LIMIT is applied. */
    excludeTeamOrigin?: boolean;
    /** Keep only team-origin rows (for hidden-count queries). */
    onlyTeamOrigin?: boolean;
    /**
     * Column to order by, all descending. 'timestamp' (default) sorts newest
     * first; 'cost' and 'duration' put the priciest / longest sessions on top,
     * with NULLs sorted last so unpriced rows never crowd out real data.
     */
    sortBy?: 'timestamp' | 'cost' | 'duration';
    /** Internal warm-cache path; callers must validate the small final result set. */
    skipExistenceCheck?: boolean;
    /**
     * Only sessions that invoked this skill (#12), joined against
     * session_resource_usage.kind='skill'. Matches either the full stored name
     * (bare, or `plugin:name` for a plugin skill) or just the short name after
     * the colon — `--skill design` finds a session that used `rush:design`.
     */
    skill?: string;
    /**
     * Only sessions that used a skill or slash-command owned by this plugin
     * (#12), joined against session_resource_usage.plugin.
     */
    plugin?: string;
}
/** Open (or return the cached) sessions database, applying migrations as needed. */
export declare function getDB(): Database.Database;
/** Close the cached database connection. */
export declare function closeDB(): void;
interface FtsOptimizeResult {
    table: string;
    segmentsBefore: number;
    segmentsAfter: number;
}
/**
 * Compact the session + tool-call full-text-search indexes.
 *
 * FTS5 appends a new segment on every insert and leaves a tombstone on every
 * delete. The scanner delete+inserts a session's docs on each rescan (and on
 * every extractor-version bump), and FTS5 never merges those segments on its
 * own — so `tool_call_text_data` / `session_text_data` accumulate hundreds of
 * thousands of unmerged segments, gigabytes of index for tens of MB of content,
 * and `agents sessions` queries slow to a crawl. The FTS5 `'optimize'` command
 * merges every segment into one and purges tombstones. Non-destructive: no
 * searchable content is lost. Reclaimed space becomes reusable free pages inside
 * the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
 */
export declare function optimizeSessionSearchIndex(): FtsOptimizeResult[];
/**
 * Keep the FTS indexes from degrading on the normal scan path.
 *
 * `optimizeSessionSearchIndex` is the full, unbounded compaction behind
 * `agents sessions optimize`. Leaving it as the ONLY compaction meant the index
 * degraded until a human happened to run that command, which is how
 * `tool_call_text_data` reached gigabytes for tens of MB of content. This is the
 * automatic counterpart: bounded, threshold-gated, and safe to call after every
 * batch of writes. Non-destructive — merging never changes what is searchable.
 *
 * Returns one result per table it actually merged (empty when every table is
 * under the threshold, which is the common case on a warm index).
 */
export declare function maintainSessionSearchIndex(db?: Database.Database, options?: {
    segmentThreshold?: number;
    mergePages?: number;
}): FtsOptimizeResult[];
/**
 * Try to claim the right to run the incremental scan. Returns true if this
 * process should proceed with scanning, false if another live process is
 * already scanning (caller should skip the scan and serve from the DB).
 *
 * Uses the `meta` table so it survives crashes — dead PIDs are detected via
 * process.kill(pid, 0), stale entries via TTL. No external lock files needed.
 *
 * Wrapped in db.transaction() (BEGIN IMMEDIATE) so the read-then-write is
 * atomic and busy_timeout retries correctly — bare auto-commit DML in WAL
 * mode can return SQLITE_BUSY_SNAPSHOT which bypasses the busy handler.
 */
export declare function tryClaimScan(pid: number): boolean;
/**
 * Read-only probe: is a scan in progress right now, held by a LIVE process
 * within the TTL? (RUSH-2682). Unlike {@link tryClaimScan} it never writes, so a
 * caller that lost the claim can wait for the in-flight scan to finish rather
 * than returning the pre-scan DB snapshot as if it were the answer. Returns
 * false for a dead-PID or expired claim — that scan is not actually running.
 */
export declare function scanInProgressByLivePid(): boolean;
/**
 * Release the scan claim written by tryClaimScan. Only deletes the entry
 * if it still belongs to this process (guards against TTL takeovers).
 */
export declare function releaseScan(pid: number): void;
/** Return the absolute path to the sessions database file. */
export declare function getDBPath(): string;
/**
 * Look up the file stat stamp we stored the last time we scanned a given file path.
 * Callers compare this to the current fs.stat to decide whether to rescan.
 */
export declare function getScanStampByPath(filePath: string): ScanStamp | null;
/**
 * Bulk-load the stamp ledger for a set of file paths in a single SQL query.
 * This is the fast path used by the incremental scanner — avoids N+1 queries.
 */
export declare function getScanStampsForPaths(filePaths: string[]): Map<string, ScanStamp>;
/**
 * A file's persisted resumable-parse continuation, read back from scan_ledger.
 * `parserState` is the serialized {@link ClaudeParserState} JSON blob (offset +
 * accumulator snapshot); `contentText` is the accumulated user doc. Both are
 * written by the Claude scan (B-2) and consumed on the next scan to decide
 * full-vs-incremental and to hydrate the resume.
 */
interface ParserStateRow {
    parserState: string | null;
    contentText: string | null;
    fileMtimeMs: number;
    fileSize: number;
    scannedAt: number;
    /** See {@link ScanStamp.extractorVersion}. A mismatch vs CONTENT_INDEX_VERSION
     *  means this continuation predates the current content extractor and MUST
     *  be treated as absent (forcing a full re-parse) rather than resumed from. */
    extractorVersion: number | null;
}
/**
 * Bulk-load the resumable-parse continuation (parser_state + content_text) plus
 * the stamp for a set of file paths in a single chunked query. Mirrors
 * {@link getScanStampsForPaths}: keys by canonical path and fans results back to
 * every original alias so callers can `.get(filePath)` with the path they passed.
 * The Claude incremental scan uses this to fetch each changed file's prior
 * continuation without an N+1 of {@link getScanStampByPath}.
 */
export declare function getParserStatesForPaths(filePaths: string[]): Map<string, ParserStateRow>;
/**
 * Record scan stamps for files we've looked at. Covers both files that produced
 * a session and files we looked at but chose not to index (e.g. malformed).
 */
export declare function recordScans(entries: Array<{
    filePath: string;
    scan: ScanStamp;
}>): void;
/** Snapshot of a leaf transcript directory used to detect create/delete/rename. */
export interface DirStamp {
    dirMtimeMs: number;
    entryCount: number;
}
/**
 * Bulk-load the dir ledger for a set of leaf directories in a single SQL query.
 * Mirrors {@link getScanStampsForPaths}: keys by canonical path (so a dir
 * reachable via a symlinked version home and its realpath collapse to one row)
 * and fans the result back out to every original alias so callers can
 * `.get(dirPath)` with the path they passed in.
 */
export declare function getDirLedgerForPaths(dirs: string[]): Map<string, DirStamp>;
/**
 * Upsert dir-scan stamps. Recorded after a full readdir of a leaf transcript
 * dir so the next scan can skip that dir when its (mtime, entry_count) is
 * unchanged. Mirrors {@link recordScans}.
 */
export declare function recordDirScans(entries: Array<{
    dirPath: string;
    dirMtimeMs: number;
    entryCount: number;
}>): void;
/**
 * Upsert a session row and replace its FTS5 content in a single transaction.
 * `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
 */
export declare function upsertSession(meta: SessionMeta, content: string, scan?: ScanStamp, assistantContent?: string): void;
/** Batch-upsert sessions with their FTS5 content and scan stamps in a single transaction. */
export declare function upsertSessionsBatch(entries: Array<{
    meta: SessionMeta;
    content: string;
    /** Assistant-answer text, accumulated the same way as `content` (the
     *  user-prompt text) but stored in session_text's own `assistant` column
     *  with a lower BM25 weight — see BM25_WEIGHTS. */
    assistantContent?: string;
    scan?: ScanStamp;
    parserState?: string;
    contentText?: string;
    events?: SessionEvent[];
    toolCalls?: IndexedToolCall[];
    toolScan?: ScanStamp;
    toolIndexMode?: 'replace' | 'append';
    /**
     * Where the NEXT scan of this full-file-harness session may resume its tool
     * index (PHNX-3411). Computed internally in the enrichment map below; not
     * supplied by callers. Persisted alongside the tool ledger so an active
     * session re-derives only its newly appended tool calls next tick.
     */
    toolResume?: ToolScanResumePoint | null;
}>): void;
/**
 * Sync labels for a set of sessions. For each id in the map, if the stored
 * label differs, update both `sessions.label` and the FTS5 label column.
 * Leaves FTS5 content/topic/project untouched — cheap to call every run.
 */
export declare function syncLabels(labelMap: Map<string, string | null>): number;
/**
 * Seed session labels from `agents run --name` handles, keyed by session id.
 *
 * `--name` is the universal launch-time way to set a session's label — the same
 * field an agent later refines with a generated title (`syncLabels`) or the user
 * with `/rename`. The seed's source of truth lives outside the transcript (host
 * task sidecars, run-name sidecars written at launch), so it is re-applied by id
 * every scan rather than parsed per-file. It only fills a label that is still
 * EMPTY — an agent-generated title always wins over the seed, so a Claude run's
 * `--name` shows until Claude titles it, and a non-Claude run keeps its `--name`
 * as the label. Writes both `sessions.label` and the FTS5 label column so a
 * seeded name is fuzzy-searchable. Cheap to call every run; returns rows updated.
 *
 * Ordering matters: this runs AFTER the per-agent scans (which apply
 * agent-generated titles via {@link syncLabels}), so it never overwrites a real
 * title — it only backfills the gap the seed was meant to cover.
 */
export declare function seedLabelsFromNames(nameMap: Map<string, string | null>): number;
/**
 * Sync topics (session titles) for a set of sessions, keyed by id. For agents
 * whose human-readable title lives in a side index that updates independently
 * of the transcript (Codex `session_index.jsonl`), the per-file scan can't see
 * a title that lands later. This applies those titles by id, updating both
 * `sessions.topic` and the FTS5 topic column. Only ever sets a non-empty title
 * and only when it differs from the stored value — cheap to call every run.
 * Returns the number of rows updated.
 */
export declare function syncTopics(topicMap: Map<string, string>): number;
export declare function isSessionActivityFresh(row: {
    last_activity: string | null;
    timestamp: string;
    file_mtime_ms: number | null;
}, maxAgeMs: number, nowMs: number): boolean;
/** Persist a lazily resolved Linear project without reparsing the transcript. */
export declare function cacheLinearProject(sessionId: string, project: string, projectUrl: string): void;
/**
 * Newest indexed session file for an agent working in `cwd`. Lets the live
 * `--active` scanner locate a Codex transcript (whose files are date-partitioned,
 * not cwd-keyed like Claude's) by reusing the index. Returns undefined if the
 * session hasn't been scanned yet — the caller degrades to no live state.
 */
export declare function latestSessionFileForCwd(agent: SessionAgentId, cwd: string, options?: {
    maxAgeMs?: number;
    nowMs?: number;
}): string | undefined;
/** Process-local diagnostics for the real-filesystem existence-cache tests. */
export declare function getSessionExistenceCacheStats(): {
    sweeps: number;
};
/** Query sessions from the database, applying filters and ordering by last-activity descending (default). */
export declare function querySessions(options?: QueryOptions): SessionMeta[];
/**
 * Cheap query for the daemon's deferred tool-index pass (PHNX-3411).
 *
 * Returns the most-recently-active sessions whose parseSession reads a large
 * flat transcript (kimi: wire.jsonl, grok: chat_history.jsonl). Their scanners
 * produce no events, so upsertSessionsBatch skips them in the warm tick to
 * avoid wedging the event loop. ensureToolIndex uses tool_scan_ledger stamps
 * to skip already-current rows and applies byte/file budget caps.
 */
export declare function querySessionsForDeferredToolIndex(limit: number): SessionMeta[];
/** Count sessions matching the given filter options. */
export declare function countSessions(options?: QueryOptions): number;
/** One grouped row in a cost/duration rollup. */
interface UsageRollupRow {
    /**
     * Grouping key value: the agent id, project name, shortened model id,
     * ISO date (YYYY-MM-DD), or account identity
     * (`claude:org=<uuid>` / `unattributed:<reason>`).
     */
    key: string;
    /**
     * Human label for the key when it is not itself readable — an org uuid is an
     * identity, not something to show a user. Absent when `key` reads fine on its own.
     */
    label?: string;
    costUsd: number;
    /**
     * USD cost priced as if caching were off (cache read/write at the input rate),
     * summed from `cost_usd_nocache`. Backs `agents insights output --pricing no-cache`.
     * Equals `costUsd` for rows whose sessions record no cache split (RUSH-2287).
     */
    costUsdNoCache: number;
    durationMs: number;
    sessionCount: number;
    tokenCount: number;
    /** Real generated (output) tokens — excludes cache-read/-write context. */
    outputTokens: number;
    /** Uncached input tokens summed across the group (0 where no harness recorded a split). */
    inputTokens: number;
    /** Cache-read tokens summed across the group. */
    cacheReadTokens: number;
    /** Cache-write (cache-creation) tokens summed across the group. */
    cacheWriteTokens: number;
}
/** What to group a usage rollup by. */
/**
 * Read cached facets for the given sessions, dropping any row that is stale.
 *
 * Staleness is decided in SQL against the session's own `file_mtime_ms` / `file_size`,
 * the same pair the scanner maintains — so the cache cannot disagree with the index,
 * `IS` rather than `=` so a source with no statable file — NULL on both sides — is a
 * cache HIT rather than a permanent miss that re-parses it on every run.
 */
export declare function readSessionInsights<T>(ids: string[]): Map<string, T>;
/**
 * Persist freshly computed facets against the stamp of the bytes actually parsed.
 *
 * The caller passes the stat it observed when it read the file. Re-reading the stamp
 * from the sessions table inside this INSERT would race: a concurrent rescan between
 * the parse and the write (the cold path flushes in batches, so the window is minutes
 * wide, and this module treats concurrent access as a design assumption) stamps NEW
 * bytes onto OLD facets — a permanent false cache hit until the file changes again.
 * tool-index.ts sets the precedent: stat at parse time, carry the stamp into the write.
 */
export declare function writeSessionInsights<T>(entries: Array<{
    id: string;
    fileMtimeMs: number | null;
    fileSize: number | null;
    facets: T;
}>): void;
/** Drop every cached facet row. Backs `agents insights --refresh`. */
export declare function clearSessionInsights(): void;
/** Read cached trace topics only when their transcript byte stamps still match. */
export declare function readSessionTopics<T>(ids: string[]): Map<string, T>;
/** Persist trace topics against the exact transcript bytes used to classify them. */
export declare function writeSessionTopics<T>(entries: Array<{
    id: string;
    fileMtimeMs: number | null;
    fileSize: number | null;
    topic: T;
}>): void;
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
export declare function readSessionPhenotypes<T>(ids: string[]): Map<string, T>;
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
export declare function writeSessionPhenotypes<T>(entries: Array<{
    id: string;
    fileMtimeMs: number | null;
    fileSize: number | null;
    phenotype: T;
}>): void;
/** Read one derived preview only when it matches the transcript bytes on disk. */
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
    fileMtimeMs: number | null;
    fileSize: number | null;
}): T | undefined;
/** Persist normalized preview data against the exact transcript bytes parsed. */
export declare function writeSessionPreviewCache<T>(entry: {
    id: string;
    fileMtimeMs: number | null;
    fileSize: number | null;
    preview: T;
}): void;
/**
 * The session's durable user-turn text, as stored in the `session_text` FTS
 * `content` column at scan time (all harnesses, keyed by session_id). This is
 * the content that survives the transcript file being deleted (RUSH-2436): the
 * render path reads it so a file-gone session still shows its user prompts, and
 * querySessions uses its presence to tell a genuine archived session (has
 * content) from a phantom (a stale/moved file_path with none). Returns undefined
 * when no row exists; an empty string when a row exists but carried no content.
 */
export declare function readSessionContent(id: string): string | undefined;
/**
 * Read the last-computed preview digest for a session by id, validated against
 * the session row's OWN stored file stamp rather than a live `fs.stat` — the
 * transcript file is gone, so there is nothing on disk to stat, but the preview
 * cache was written with the same `file_mtime_ms`/`file_size` the sessions row
 * still records, so the join re-establishes that stamp. Backs the file-gone
 * render/preview path (RUSH-2436). Returns undefined when no cached digest
 * survives for this session.
 */
export declare function readArchivedSessionPreview<T>(id: string): T | undefined;
/** The daemon-computed summary stored in `session_summaries` (PHNX-3939). */
export interface SessionSummaryEntry {
    goal?: string;
    checkpoints?: SessionCheckpoint[];
    summaryChecklist?: SessionChecklistItem[];
    summaryState: SummaryState;
}
/**
 * Read one summary only when it matches the transcript bytes on disk (the
 * reuse-or-recompute stamp the SessionSummarizerService gates on). Mirrors
 * {@link readSessionPreviewCache}.
 */
export declare function readSessionSummary(id: string, sourceStamp: {
    fileMtimeMs: number | null;
    fileSize: number | null;
}): SessionSummaryEntry | undefined;
/**
 * Read the latest stored summary by session id, regardless of the transcript
 * stamp — the low-cost merge read used by the display path (a live row through
 * {@link applyImmutableMemo} or a history/mirror row) to surface the
 * last-computed summary without a model call. A slightly-stale summary is the
 * best available until the service recomputes; the SERVICE uses the stamped
 * {@link readSessionSummary} to decide whether to recompute.
 */
export declare function readSessionSummaryAny(id: string): SessionSummaryEntry | undefined;
/** Persist a computed summary against the exact transcript bytes it was derived from. */
export declare function writeSessionSummary(entry: {
    id: string;
    fileMtimeMs: number | null;
    fileSize: number | null;
    summary: SessionSummaryEntry;
}): void;
/** A local-origin session, projected to the compact fields the fleet mirror publishes. */
export interface LocalMirrorSource {
    id: string;
    shortId: string;
    agent: string;
    version: string | null;
    machine: string | null;
    cwd: string | null;
    topic: string | null;
    firstUserMessage: string | null;
    label: string | null;
    lastActivity: string | null;
    timestamp: string;
    ticketId: string | null;
    prUrl: string | null;
    /** Daemon-computed summary (PHNX-3939), so peers carry it without a transcript. */
    summary: SessionSummaryEntry | null;
}
/**
 * The most-recently-active sessions whose transcript is genuinely local to this
 * box — a real `file_path`, not a peer mirror — for publishing to the fleet
 * session mirror (PHNX-3792). Bounded and team-origin-excluded so the published
 * payload stays the size a picker would show. Never returns a row this box is
 * itself mirroring from a peer (`mirror_synced_at IS NULL`).
 */
export declare function queryLocalOriginSessionsForMirror(self: string, limit: number): LocalMirrorSource[];
/** One peer session digest to write into this box's local mirror. */
export interface MirrorSessionUpsert {
    id: string;
    shortId: string;
    agent: string;
    version?: string | null;
    machine: string;
    cwd?: string | null;
    topic?: string | null;
    firstUser?: string | null;
    label?: string | null;
    lastActivity?: string | null;
    timestamp: string;
    ticketId?: string | null;
    prUrl?: string | null;
    /** Daemon-computed summary carried from the publishing peer (PHNX-3939). */
    summary?: SessionSummaryEntry | null;
}
/**
 * Write one peer session's digest into the local `sessions` index as a mirror
 * row (PHNX-3792), so the picker/list/focus render its topic/preview inline
 * with no per-row SSH. The upsert is GUARDED: it never overwrites a genuine
 * local transcript row (`file_path` non-empty and not itself a mirror), so a
 * mirror can only create a new peer-only row or enrich an existing empty-file
 * host-dispatch stub / older mirror. Returns true when a row was written (the
 * caller then persists the matching preview digest); false when a real row was
 * protected. `machine` carries the publisher's recorded execution host so the
 * row collapses with the live fan-out row under the same `machine:id` key.
 */
export declare function upsertMirrorSession(row: MirrorSessionUpsert, source: string, syncedAt: number): boolean;
/**
 * Drop peer mirror rows (and their cached digests) whose last sync predates the
 * cutoff — the size ceiling / staleness pruner for the fleet session mirror
 * (PHNX-3792). Only ever touches mirror rows (`mirror_synced_at IS NOT NULL`),
 * never a genuine local or host-dispatch row. Returns the number of rows pruned.
 */
export declare function pruneMirrorSessions(cutoffMs: number): number;
/** Plugin provenance already indexed for resources used by one session. */
export declare function getSessionPlugins(id: string): string[];
export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'model' | 'account';
/**
 * Smart-launch affinity priors: group sessions by origin machine, harness, or
 * joint (machine + agent). Ordered by launch count desc.
 *
 * SQL shape (device example):
 *   SELECT machine, COUNT(*) launches, SUM(duration_ms), SUM(token_count)
 *   FROM sessions
 *   WHERE timestamp >= ? AND origin = 'cli' AND is_team_origin = 0
 *   GROUP BY machine ORDER BY launches DESC
 *
 * Account rotation is NOT done here — that stays on live rate-limit windows
 * via `--strategy balanced` / rotate.ts.
 */
type AffinityGroup = 'machine' | 'agent' | 'machine_agent';
export interface AffinityRow {
    /** Group key: machine name, agent id, or "machine\\tagent". */
    key: string;
    machine?: string;
    agent?: string;
    launches: number;
    durationMs: number;
    tokenCount: number;
    costUsd: number;
}
export declare function queryAffinityRollup(options: {
    groupBy: AffinityGroup;
    /** ISO cutoff or ms; defaults to 14 days ago when omitted. */
    sinceMs?: number;
    /** Restrict to these harnesses (e.g. claude/codex/kimi). */
    agents?: SessionAgentId[];
    /** Default true: only origin=cli rows. */
    onlyCli?: boolean;
    /** Default true: drop team-spawned sessions. */
    excludeTeamOrigin?: boolean;
    project?: string;
}): AffinityRow[];
/**
 * Aggregate cost / duration / tokens across sessions, grouped by agent,
 * project, shortened model id, account, or calendar day. Honors the same
 * filter shape as querySessions (agent, since/until, team-origin) so
 * `agents insights cost --since 7d --by day` lines up with what
 * `agents sessions` would list. Ordered by cost desc.
 */
export declare function queryUsageRollup(options: QueryOptions & {
    groupBy: UsageRollupGroup;
}): UsageRollupRow[];
/** One aggregated resource (skill or slash-command) in a usage-stats rollup. */
export interface ResourceStatRow {
    /** 'skill' or 'command' (singular, as stored in session_resource_usage.kind). */
    kind: string;
    /** Stored resource name — bare, or `plugin:short` for a plugin-owned resource. */
    name: string;
    /** Owning plugin, or null for a flat (non-namespaced) resource. */
    plugin: string | null;
    /** DotAgents layer or plugin marketplace the resource resolved to at write time. */
    source: string | null;
    /** Distinct sessions that invoked this resource within the filter window. */
    sessions: number;
    /** Total invocations (sum of per-session counts) within the window. */
    invocations: number;
}
/**
 * Roll up skill / slash-command usage from session_resource_usage, joined to
 * `sessions` for attribution so the same filter shape as querySessions
 * (agent / project / since / machine) narrows WHICH sessions count. Grouped by
 * resource identity (kind + name + plugin + source) and ordered by invocation
 * volume — the read side of "which skills/commands do I actually use, and which
 * are dead weight". `order: 'bottom'` ranks least-used first (the one-time
 * skills); `limit` caps the returned rows.
 *
 * The signal only captures EXPLICIT invocations (slash commands and `Skill`
 * tool calls). An auto-triggered skill (loaded by description match) emits no
 * event, so it reads as zero here — a 0 means "never explicitly invoked", not
 * "never loaded". Skill invocations are recorded for Claude and Kimi (the
 * `Skill`-tool harnesses); slash-commands are Claude-only.
 *
 * `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
 * distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
 * — deliberately not routed through buildSessionWhere so `--plugin rush` shows
 * only rush's resources rather than every resource used by a rush-touching
 * session.
 */
export declare function queryResourceUsageStats(options: QueryOptions & {
    kind?: 'skill' | 'command';
    pluginFilter?: string;
    order?: 'top' | 'bottom';
    limit?: number;
}): ResourceStatRow[];
/**
 * Coverage of the resource-usage signal, as three honest facts:
 *
 * - `scanned`  — sessions the resource extractor has actually processed, i.e.
 *   those carrying a `resource_scan_ledger` row at the current
 *   `RESOURCE_INDEX_VERSION`. This is the true "has the historical backfill run"
 *   signal: the ledger is stamped for EVERY scanned session, including ones that
 *   invoked nothing (`resource_count = 0`), so `scanned/total` rises to ~1 after
 *   `agents sessions backfill resources` regardless of how sparse explicit
 *   invocations are.
 * - `covered`  — distinct sessions that carry AT LEAST ONE row in
 *   `session_resource_usage`, i.e. that actually recorded an explicit invocation.
 *   This is an ABSOLUTE signal count, not a coverage ratio: it stays small even
 *   at full scan coverage because most sessions invoke no skill/command, and a
 *   non-recording harness contributes none by construction.
 * - `total`    — sessions indexed.
 *
 * The two were previously conflated: `covered/total` was framed as coverage and
 * read ~1.2% even after a full backfill (most sessions genuinely invoke nothing),
 * so the "run the backfill" hint never cleared. Keying the hint on `scanned/total`
 * fixes that — see `commands/sessions-stats.ts` (PHNX-2301).
 */
export declare function resourceUsageCoverage(): {
    covered: number;
    scanned: number;
    total: number;
};
/** Outcome of a resource-usage backfill run. */
interface ResourceBackfillResult {
    /** Sessions considered (matched the filter, had a real transcript). */
    scanned: number;
    /** Sessions (re)parsed and written this run. */
    updated: number;
    /** Sessions already current at this extractor version, skipped. */
    skipped: number;
    /** Sessions whose transcript could not be stat'd or parsed. */
    failed: number;
    /** Total session_resource_usage rows written across updated sessions. */
    resourceRows: number;
}
/**
 * One-shot historical backfill of session_resource_usage (#12). The normal
 * incremental scan writes resource usage only for sessions whose transcript it
 * (re)parses; a session indexed before this feature shipped keeps a fresh
 * scan_ledger row and is never re-derived, so its skill/slash-command tallies
 * were never recorded. This walks the session index, re-parses each transcript
 * FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
 * claude/codex re-derive their tallies from scratch), writes the usage, and
 * stamps resource_scan_ledger so reruns skip completed transcripts — the same
 * independent-ledger shape `agents sessions backfill tools` uses.
 *
 * Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
 * every agent uniformly, so there is no per-harness branch to keep in parity.
 * Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
 */
export declare function backfillResourceUsage(filter?: QueryOptions, onProgress?: (done: number, total: number) => void): ResourceBackfillResult;
/** Who spawned a team: the orchestrator session, from its transcript. */
export interface TeamSpawner {
    sessionId: string;
    shortId: string;
    /** The human the orchestrator ran as, when the row carries actor provenance. */
    actor?: string;
}
/**
 * Map every team name to the session that ran `agents teams create/add` for it.
 *
 * One scan over the rows that carry a `spawned_team`, rather than a query per
 * team — `agents teams list` needs the whole map at once, and the column has no
 * index. When two sessions spawned the same team name (a team re-created after a
 * disband), the most recent wins, which is the one whose work the name refers to.
 */
export declare function teamSpawners(): Map<string, TeamSpawner>;
/** A session with its cost, for the top-N-by-cost listing. */
interface TopCostSession {
    meta: SessionMeta;
    costUsd: number;
    durationMs: number;
}
/**
 * Return the N most expensive sessions (cost_usd DESC, NULLs excluded),
 * honoring the same filter shape as querySessions. Drops rows whose JSONL
 * vanished, mirroring querySessions' liveness filter.
 */
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
/**
 * Read machine attribution in batches without materializing full sessions.
 * Failure is best-effort so the live view can still render local attribution.
 */
export declare function findSessionMachinesByIds(ids: string[]): Map<string, string>;
export declare function getSessionById(id: string): SessionMeta | null;
/** Exact ids win over prefixes; the normal existence check preserves archived content but excludes phantoms. */
export declare function findSessionsById(idQuery: string, scope?: Pick<QueryOptions, 'agent' | 'version' | 'cwd' | 'project'>): SessionMeta[];
/**
 * Upgrade a session-id crumb to the full indexed id. The 8-char hex `short_id`
 * a "Sent from …" footer shows would 404 as a `…/console/sessions/<id>` URL, so
 * an owner ping resolves it through the index first (PHNX-3698). A value that is
 * already a full id (or any non-crumb shape, e.g. a native `ses_…` id), or a
 * crumb the index cannot resolve, is returned unchanged — the caller keeps
 * exactly what it had rather than getting a fabricated id.
 */
export declare function resolveFullSessionId(idOrCrumb: string | undefined): string | undefined;
/** Batch-resolve pane short ids; on collision the most recently active session wins. */
export declare function findSessionsByShortIds(shortIds: string[]): Map<string, SessionMeta>;
/** A single full-text search result with ranking score. */
interface FtsHit {
    sessionId: string;
    score: number;
    matchedTerms: string[];
    /**
     * A short bm25 `snippet()` excerpt around the best-matching column (label,
     * topic, project, user content, or assistant answer), with the matched
     * term(s) wrapped in `**…**`. Absent for a handle/label-tier hit (tiers 1-3
     * below), which has no excerpt to show — the label itself IS the match.
     */
    snippet?: string;
}
/**
 * Escape a raw user query into a safe FTS5 MATCH expression.
 * Splits on non-word characters, keeps tokens >= 2 chars, and OR-joins
 * them with a prefix wildcard so partial typing ('rush dep') matches.
 */
export declare function buildFtsQuery(input: string): {
    expr: string;
    terms: string[];
};
/**
 * Label-first search. Sessions whose custom label substring-matches the query
 * always rank ahead of FTS5 hits — this gives predictable behavior when a user
 * types the exact name they gave a session via /rename.
 *
 * Tiers (highest → lowest):
 *   1. Exact label match (case-insensitive): score 1_000_000
 *   2. Label prefix match:                   score   900_000
 *   3. Label contains query:                 score   800_000
 *   4. FTS5 BM25 hits:                       score   1..1000 (scaled)
 *
 * Note: FTS5's bm25() returns negative numbers; we flip the sign for tier 4
 * so "higher = better" is consistent across all tiers.
 */
export declare function ftsSearch(input: string, limit?: number): FtsHit[];
/**
 * Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
 * it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
 * entries so they are re-indexed from the new location on the next scan.
 *
 * Used by removeVersion after soft-deleting a version directory to trash, so
 * that session reads (transcript view, /continue) still work from the trash path.
 * Returns the number of session rows updated.
 */
export declare function updateSessionFilePaths(oldPrefix: string, newPrefix: string): number;
/** Per-kind capture tallies for a browser task. Counts only -- never the bytes. */
interface BrowserCaptureCounts {
    screenshot: number;
    pdf: number;
    recording: number;
    download: number;
}
/** One durable browser-task row. `machine` defaults to this device. */
interface BrowserSessionRecord {
    task: string;
    profile: string;
    sessionId?: string;
    launchId?: string;
    actor?: string;
    machine?: string;
    startedAt?: number;
    lastActivity?: number;
    counts?: Partial<BrowserCaptureCounts>;
    captureDir?: string;
    capturesRemote?: string;
}
/** One durable computer-use invocation row. `machine` defaults to this device. */
interface ComputerSessionRecord {
    invocationId: string;
    sessionId?: string;
    launchId?: string;
    actor?: string;
    machine?: string;
    startedAt?: number;
    lastActivity?: number;
    actionCount?: number;
    taskPreview?: string;
}
/**
 * Upsert one browser task's durable metadata.
 *
 * Called at task START. This executes INSIDE the browser daemon (daemon.ts
 * constructs BrowserService; ipc.ts dispatches `start` to it), which makes the
 * shared daemon a writer of this DB — but the identity it writes is RESOLVED IN
 * THE CALLING CLI PROCESS and forwarded over IPC, never resolved here. That
 * distinction is the whole fix: resolving it daemon-side would attribute every
 * task to the daemon's own actor (the RUSH-2020 bug).
 *
 * `agents browser stop` deliberately does NOT delete this row: the whole point
 * is that the link outlives the task.
 *
 * Identity fields are only ever widened, never blanked. A later capture-count
 * update carries no session id, and `COALESCE(excluded.…, browser_sessions.…)`
 * keeps the one recorded at start rather than overwriting it with NULL --
 * otherwise the second write would undo exactly what the first was for.
 */
export declare function recordBrowserSession(record: BrowserSessionRecord): void;
/**
 * Upsert one computer-use invocation's durable metadata.
 *
 * `action_count` accumulates: a `computer run` loop emits many actions under one
 * invocation id, and each arrives as its own call, so the count is incremented
 * rather than replaced. Identity is widened-only, for the same reason as above.
 */
export declare function recordComputerSession(record: ComputerSessionRecord): void;
/** A stored browser row as read back, with counts rehydrated. */
interface StoredBrowserSession extends Required<Pick<BrowserSessionRecord, 'task' | 'profile'>> {
    sessionId?: string;
    launchId?: string;
    actor?: string;
    machine: string;
    startedAt: number;
    lastActivity?: number;
    counts: BrowserCaptureCounts;
    captureDir?: string;
    capturesRemote?: string;
}
/**
 * Stored browser tasks, newest first; optionally scoped to one profile.
 *
 * A PROFILE-SCOPED read is deliberately unbounded. These rows are the identity
 * for capture dirs that still exist on disk, and dropping the oldest ones would
 * silently regress exactly those tasks to `unlinked` — the symptom this whole
 * change removes, reappearing at a higher threshold. The row count is already
 * bounded by the tasks that profile has ever run, and each row is a short
 * metadata record. Only the unscoped read (every profile, used for overviews)
 * takes a ceiling, since that one has no natural bound.
 */
export declare function listBrowserSessionRecords(profile?: string, opts?: {
    limit?: number;
}): StoredBrowserSession[];
/** One stored browser task by its (profile, task) key, or null. */
export declare function getBrowserSessionRecord(profile: string, task: string): StoredBrowserSession | null;
/** A stored computer-use invocation as read back. */
interface StoredComputerSession {
    invocationId: string;
    sessionId?: string;
    launchId?: string;
    actor?: string;
    machine: string;
    startedAt: number;
    lastActivity?: number;
    actionCount: number;
    taskPreview?: string;
}
/**
 * Drop tool-session rows past {@link TOOL_SESSION_MAX_AGE_DAYS}.
 *
 * Called from the listing path, never from the write hot path: an
 * `agents computer` action must not pay for a table sweep (see the fail-soft
 * note on `recordComputerSession`'s caller). Returns rows deleted.
 */
export declare function pruneToolSessions(maxAgeDays?: number): number;
/**
 * Stored computer-use invocations, newest first, bounded.
 *
 * The limit is a real ceiling, not a nicety: `--json` serializes whatever this
 * returns, so an unbounded read would dump the entire table on every call.
 */
export declare function listComputerSessionRecords(opts?: {
    limit?: number;
    startedBeforeMs?: number;
}): StoredComputerSession[];
export {};
