import type { SkillUsageEntry, TokenUsage } from '@auden.to/protocol'; export type SessionUsage = { sessionId: string; usage: TokenUsage[]; }; export type SessionSkillUsage = { sessionId: string; usage: SkillUsageEntry[]; }; export type ExtractedRunUsage = { runUsage: SessionUsage[]; skillUsage: SessionSkillUsage[]; }; /** * Which transcripts an extraction may read, and how they were chosen. * * Two modes on purpose (docs/plans/default-transcript-discovery-plan.md → * "Two modes, not one"): * * - `sessions` is the automatic path. The caller has already resolved the * sessions this sync is uploading, so the work is bounded by sessions that * have runs rather than by the machine's whole agent history. * - `scan` is what an explicit `--transcripts-dir` selects. Enumerating the * root is precisely what makes it a backfill; narrowing it to the current * upload's sessions would silently drop the historical sessions users pass * the flag for. * * Absent (or `null`) means "read nothing" — which is also how `--no-actions` * is honoured, at the single point that resolves this value. */ /** * One transcript to read, and the session it was resolved *for*. * * `sessionId` is a fallback, not an override: a record's own `sessionId` still * wins. It matters when a transcript is not named for its session — the case * the reported hook path exists to resolve — because the only other fallback * is the filename, which would then credit the usage to a session id no run * has, and the server would discard it (`sync.usage_no_matching_run`). Absent * when the file was found by scanning, where nothing knows which session asked. */ export type ResolvedTranscript = { path: string; sessionId?: string; }; export type TranscriptSource = { mode: 'sessions'; files: ResolvedTranscript[]; } | { mode: 'scan'; dir: string; }; export type ExtractRunUsageOptions = { /** * Where to keep the byte-offset cursor and cached per-session totals. * * Omitted (or `null`) reads every transcript in full, which is what `scan` * mode always does — a backfill is a full re-read by definition, and it is * the mode where one session's records most plausibly appear in more than one * file. Automatic `sessions` mode passes a path, because there the Stop hook * re-reads the same growing transcript on every assistant turn. */ cachePath?: string | null; }; /** * Extracts per-session, per-model token usage from Claude Code JSONL * transcripts (docs/plans/token-cost-tracking-plan.md §5.1), plus per-skill * Tier-1 "load overhead" usage (§13.3): when an assistant message requests the * `Skill` tool, the *next* assistant message's usage is the first-load cost of * that skill (v1 approximation — see §13.3's caching nuance). Opening a * `SKILL.md` no longer counts, and `detectSkillLoadSignal` says why. Reads only * `usage`/`model`/`sessionId` and tool_use `name`/`input` off assistant * messages — never message text. * * Each message carries its own (non-cumulative) usage, so we sum every * assistant message's usage per (session, model). Messages are deduped by * their id so re-reading a transcript that grew since the last sync does not * double-count lines already summed. * * **Dedup is scoped to the session, not the whole extraction.** Claude Code * writes one record per *content block* of an assistant message, all sharing a * `message.id` and an identical `usage` object, so dropping the repeats is * load-bearing — one measured session sums 2.5× too high without it. But the * set used to be global across files and was tested *before* the session id was * resolved, over lexicographically sorted paths: a message id appearing in two * transcripts silently dropped the later session's tokens, with filename sort * as the tiebreak. Per-run cost is the only dedup semantics a session-scoped * extractor can compute — it never sees the sessions it would have to dedup * against — so the account rollup is the sum of these per-run totals, and * over-counts by any replayed overlap between sessions * (docs/plans/default-transcript-discovery-plan.md → "Dedup scope"). * * **Given a cache path, only bytes appended since the last call are read.** The * fold is a pure left fold over append-only lines, so resuming from a byte * offset with the accumulated state produces the totals a cold read would — * which matters because `ingestRunUsage` replaces per `(runId, model, source)` * with full-session totals, so the totals must still be complete, they just * need not be recomputed from the whole file. Every failure mode here degrades * to a re-parse rather than a wrong number. */ export declare function extractRunUsage(source?: TranscriptSource | null, options?: ExtractRunUsageOptions): Promise; //# sourceMappingURL=extract.d.ts.map