/** * Session mining for `/learn`. * * Reads session `.jsonl` files straight off disk rather than the live context. * That is the whole point: the on-disk transcript is complete even when the * in-context one has been compacted away, and it spans every past session * instead of only this one. Cross-session repetition is the signal that decides * whether something is a durable rule or a one-off, and it is the one thing a * prompt reading its own context cannot see. * * This module is the orchestrator, and the split of labour inside it is * deliberate: * * - **Gathering** is deterministic. Finding session files, resolving which cwd * they belong to, walking the active branch of a forked session — all exact, * all cheap, all here. * - **Judgement** is the model's, in `mine.ts` and `coverage.ts`. What counts as * a directive, what two phrasings have in common, whether a rule already * covers something — none of that survives contact with a regex, and it used * to be decided by one. * - **Counting** is deterministic again, in `reduce.ts`. The number is the * product, and a model asked to count over a long context will be * approximately right. * * The expensive step is memoized per session file (`cache.ts`), so a session is * read by the model exactly once in its life and the counts are still computed * over every session in the window on every run. */ import type { Clusterer } from "./cluster.js"; import type { CoverageIndex, CoverageJudge } from "./coverage.js"; import type { Miner } from "./mine.js"; import type { DirectiveCluster, FixCandidate, RequestCandidate } from "./reduce.js"; import { type LearnState } from "./state.js"; export type { CoverageIndex, CoverageMatch } from "./coverage.js"; export { LEARN_DIGEST_MARKER } from "./mine.js"; export type { DirectiveCluster, DirectiveStatus, FixCandidate, RequestCandidate } from "./reduce.js"; /** * Why a session file on disk did not make it into the digest. * * "No recent sessions" is the one outcome a user cannot act on without this: * an empty session directory, a directory full of month-old sessions, and a * directory full of sessions belonging to another checkout all produce the same * sentence, and the fix differs in each case. */ export interface SessionScanReport { /** Directories actually searched, in order. */ dirs: string[]; /** Directories that do not exist on disk. */ missingDirs: string[]; /** `.jsonl` files found across all searched directories. */ files: number; /** Skipped for being older than the age window. */ tooOld: number; /** Skipped because the session header records a different working directory. */ otherCwd: number; /** Skipped for being beyond `maxSessions`. */ overLimit: number; /** Skipped for being unreadable, unparseable, or empty. */ unreadable: number; } /** What the run cost, so the price of an LLM-read pipeline is visible rather than hidden. */ export interface MiningReport { /** Sessions whose candidates came from cache, free. */ cached: number; /** Sessions sent to the model this run. */ mined: number; /** Sessions the model failed on. Their signals are missing from the counts. */ failed: number; } export interface LearnDigest { scannedSessions: number; skippedSessions: number; /** Where the sessions came from, and what was passed over. */ scan: SessionScanReport; /** What was read by the model versus reused. */ mining: MiningReport; /** * The run stopped before reading the whole window, so the counts below are * computed from part of it. Callers must not record these as surfaced: a * partial count can fall under the repeat threshold, and bookmarking it would * hide the item on the next run, when the evidence is complete. */ aborted: boolean; /** * The coverage judge failed, so every directive reads `new` whether or not it * is written down. Callers must not record these as surfaced either: the * bookmark stores whether an item was covered when shown, and a wrong `false` * there tells a later run you passed over a proposal you were never given. */ coverageFailed: boolean; oldestSession?: string; newestSession?: string; agentsFilePath?: string; agentsFileTokens?: number; directives: DirectiveCluster[]; fixes: FixCandidate[]; requests: RequestCandidate[]; /** Items held back because nothing new has happened since they were last shown. */ suppressed: number; /** Items that cleared every threshold but lost the ranking to `maxProposals`. */ cut: number; /** * What the window contained before the thresholds, so an empty digest can be * read. * * The pipeline filters hard — replayed slash-command bodies, tool output, * quotes that cannot be found in the transcript, then a distinct-session bar * — and every one of those is silent. Without these numbers "nothing to * propose" is unreadable: it could mean the sessions taught nothing, or that * the bar is one session too high, and the reader has no way to tell which * knob to reach for. */ funnel: { /** Occurrences the miner reported and the quote check accepted. */ candidates: number; /** Distinct points after naming — how much the clustering pass actually merged. */ points: number; /** Points that were named and counted but did not clear the repeat threshold. */ belowThreshold: number; }; /** Everything this run put on screen, for the caller to persist. */ surfaced: Array<{ key: string; lastSeen: string; covered: boolean; text?: string; }>; } export interface ExtractOptions { cwd: string; agentDir: string; /** * An extra directory to scan, normally the live session manager's. The * per-cwd default directory is always scanned as well, so a session manager * pointing somewhere unusual cannot hide this directory's history. */ sessionDir?: string; maxSessions?: number; maxAgeDays?: number; /** Occurrences a directive needs before it is proposed. The signal/noise dial. */ minRepeats?: number; /** Repeats a tool sequence needs before it is proposed as a skill. */ minRequestRepeats?: number; /** Cap on each list in the digest. */ maxProposals?: number; /** * What previous runs already showed. Items with no new occurrences since are * held back. Omit (or pass `ignoreState`) to propose everything in the window. */ state?: LearnState; /** Re-propose everything, ignoring what previous runs surfaced (`/learn all`). */ ignoreState?: boolean; /** * Skills a directive can already be covered by. Defaults to the ones loaded * from disk; injectable so tests do not read the developer's real skills. */ skills?: Array<{ name: string; description: string; }>; /** Injectable clock, for tests. */ now?: Date; } /** Everything the async pipeline needs beyond the window settings. */ export interface MineOptions extends ExtractOptions { /** Reads one session and reports what it saw. */ miner: Miner; /** * Names the whole window at once, deciding which occurrences are the same * point. Without one, each candidate is named after its own wording, which * groups identical sentences and nothing else. */ clusterer?: Clusterer; /** Decides which proposals are already written down. Defaults to "none are". */ coverageJudge?: CoverageJudge; /** Progress callback, so a cold-cache run is not a silent wait. */ onProgress?: (progress: { done: number; total: number; cached: number; }) => void; signal?: AbortSignal; } /** * Every directory this cwd's sessions could be sitting in. * * The caller passes the live session manager's directory, which is the right * answer almost always — but not quite always, and each exception silently * emptied the digest. An in-memory session (`--no-session`) reports `""`; an * explicit `--session ` reports wherever that file lives; a custom * `sessionDir` setting points at one shared directory. In every one of those * cases the per-cwd default directory still holds the history worth mining, so * search both and let the header check sort out what belongs to this cwd. */ export declare function candidateSessionDirs(options: Pick): string[]; /** * Where this cwd's sessions were found and what was passed over, without * mining anything. `/learn settings` and `/learn stats` report on the window * without paying for a model call. */ export declare function scanSessions(options: ExtractOptions): SessionScanReport; /** * What a run would read, without reading it. * * Runs the real selection — the same age, cwd, cap and de-duplication rules * `mineLearnDigest` applies — and then asks the cache about each survivor. It * has to be the same selection: this number is what the confirmation prompt * quotes, and a prompt that says twelve before reading three is worse than no * prompt at all. Hashing the chosen files is cheap next to sending them to a * model. */ export declare function planMining(options: ExtractOptions): { total: number; cached: number; pending: number; }; /** Assemble the coverage index for a directory. */ export declare function buildCoverageIndex(options: { cwd: string; agentDir: string; skills?: Array<{ name: string; description: string; }>; }): CoverageIndex; /** Mine the recent sessions for this cwd and return the ranked digest. */ export declare function mineLearnDigest(options: MineOptions): Promise; //# sourceMappingURL=extract.d.ts.map