mossbear 0.1.0-alpha.57 → 0.1.0-alpha.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import type { ExtractedSessionUsage, ResolvedTranscript } from './types.js';
2
+ /** One agent's transcript location, filename convention, and usage parser. */
3
+ export type TranscriptUsageAdapter = {
4
+ id: string;
5
+ roots(env: NodeJS.ProcessEnv): string[];
6
+ matchesPath(path: string): boolean;
7
+ sessionIdFromFilename(filename: string): string | null;
8
+ extract(files: readonly ResolvedTranscript[], cachePath?: string | null): Promise<ExtractedSessionUsage>;
9
+ };
10
+ export declare const TRANSCRIPT_USAGE_ADAPTERS: readonly TranscriptUsageAdapter[];
11
+ //# sourceMappingURL=adapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../src/usage/adapters.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE3E,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,CAAA;IACvC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtD,OAAO,CACL,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAAC,qBAAqB,CAAC,CAAA;CAClC,CAAA;AAID,eAAO,MAAM,yBAAyB,EAAE,SAAS,sBAAsB,EA0BtE,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { homedir } from 'node:os';
2
+ import { basename, extname, join } from 'node:path';
3
+ import { extractClaudeUsage } from './claude.js';
4
+ import { extractCodexUsage } from './codex.js';
5
+ const codexRollout = /^rollout-.*-([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})\.jsonl$/;
6
+ export const TRANSCRIPT_USAGE_ADAPTERS = [
7
+ {
8
+ id: 'claude',
9
+ roots: (env) => [
10
+ ...(env.CLAUDE_CONFIG_DIR?.trim()
11
+ ? [join(env.CLAUDE_CONFIG_DIR.trim(), 'projects')]
12
+ : []),
13
+ join(homedir(), '.claude', 'projects'),
14
+ ],
15
+ matchesPath: (path) => extname(path) === '.jsonl' && !basename(path).startsWith('rollout-'),
16
+ sessionIdFromFilename: (filename) => filename.endsWith('.jsonl') && !filename.startsWith('rollout-')
17
+ ? basename(filename, '.jsonl')
18
+ : null,
19
+ extract: extractClaudeUsage,
20
+ },
21
+ {
22
+ id: 'codex',
23
+ roots: (env) => [
24
+ join(env.CODEX_HOME?.trim() || join(homedir(), '.codex'), 'sessions'),
25
+ ],
26
+ matchesPath: (path) => codexRollout.test(basename(path)),
27
+ sessionIdFromFilename: (filename) => codexRollout.exec(filename)?.[1] ?? null,
28
+ extract: extractCodexUsage,
29
+ },
30
+ ];
31
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../../src/usage/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAgB9C,MAAM,YAAY,GAAG,mEAAmE,CAAA;AAExF,MAAM,CAAC,MAAM,yBAAyB,GAAsC;IAC1E;QACE,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;YACd,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE;gBAC/B,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;gBAClD,CAAC,CAAC,EAAE,CAAC;YACP,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC;SACvC;QACD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CACpB,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;QACtE,qBAAqB,EAAE,CAAC,QAAQ,EAAE,EAAE,CAClC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;YAC7D,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;YAC9B,CAAC,CAAC,IAAI;QACV,OAAO,EAAE,kBAAkB;KAC5B;IACD;QACE,EAAE,EAAE,OAAO;QACX,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;YACd,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,UAAU,CAAC;SACtE;QACD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxD,qBAAqB,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;QAC7E,OAAO,EAAE,iBAAiB;KAC3B;CACF,CAAA"}
@@ -0,0 +1,38 @@
1
+ import type { ExtractedSessionUsage, ResolvedTranscript } from './types.js';
2
+ /**
3
+ * Extracts per-session, per-model token usage from Claude Code JSONL
4
+ * transcripts, plus per-skill
5
+ * Tier-1 "load overhead" usage (§13.3): when an assistant message requests the
6
+ * `Skill` tool, the *next* assistant message's usage is the first-load cost of
7
+ * that skill (v1 approximation — see §13.3's caching nuance). Opening a
8
+ * `SKILL.md` no longer counts, and `detectSkillLoadSignal` says why. Reads only
9
+ * `usage`/`model`/`agentSessionId` and tool_use `name`/`input` off assistant
10
+ * messages — never message text.
11
+ *
12
+ * Each message carries its own (non-cumulative) usage, so we sum every
13
+ * assistant message's usage per (session, model). Messages are deduped by
14
+ * their id so re-reading a transcript that grew since the last sync does not
15
+ * double-count lines already summed.
16
+ *
17
+ * **Dedup is scoped to the session, not the whole extraction.** Claude Code
18
+ * writes one record per *content block* of an assistant message, all sharing a
19
+ * `message.id` and an identical `usage` object, so dropping the repeats is
20
+ * load-bearing — one measured session sums 2.5× too high without it. But the
21
+ * set used to be global across files and was tested *before* the session id was
22
+ * resolved, over lexicographically sorted paths: a message id appearing in two
23
+ * transcripts silently dropped the later session's tokens, with filename sort
24
+ * as the tiebreak. Per-run cost is the only dedup semantics a session-scoped
25
+ * extractor can compute — it never sees the sessions it would have to dedup
26
+ * against — so the account rollup is the sum of these per-run totals, and
27
+ * over-counts by any replayed overlap between sessions.
28
+ *
29
+ * **Given a cache path, only bytes appended since the last call are read.** The
30
+ * fold is a pure left fold over append-only lines, so resuming from a byte
31
+ * offset with the accumulated state produces the totals a cold read would —
32
+ * which matters because `ingestSessionUsage` replaces per `(sessionId, model, source)`
33
+ * with full-session totals, so the totals must still be complete, they just
34
+ * need not be recomputed from the whole file. Every failure mode here degrades
35
+ * to a re-parse rather than a wrong number.
36
+ */
37
+ export declare function extractClaudeUsage(files: readonly ResolvedTranscript[], cachePath?: string | null): Promise<ExtractedSessionUsage>;
38
+ //# sourceMappingURL=claude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/usage/claude.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAmM3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAAC,qBAAqB,CAAC,CAGhC"}
@@ -0,0 +1,497 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { open, readFile, stat } from 'node:fs/promises';
3
+ import { basename, extname } from 'node:path';
4
+ import { inferProviderFromModel } from './providers.js';
5
+ import { emptyTranscriptUsageCache, readTranscriptUsageCache, touchCacheOrder, writeTranscriptUsageCache, } from './usage-cache.js';
6
+ function asRecord(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
8
+ return null;
9
+ }
10
+ return value;
11
+ }
12
+ /**
13
+ * Detects a skill-load signal in one assistant message's content blocks:
14
+ * an explicit `Skill` tool_use,
15
+ * whose `input.skill` names the skill. Only the first signal in a message is
16
+ * used — a message that requests two skill loads at once is a rare edge case
17
+ * not worth the double-counting risk of attributing the next message's usage to
18
+ * both.
19
+ *
20
+ * **A `SKILL.md` in a tool call's path used to count too, and must not.** It
21
+ * matched on basename alone across *any* tool carrying a path — `Edit` and
22
+ * `Write` included — so writing `/repo/docs/SKILL.md` recorded a load of a
23
+ * skill named `docs`, and reading a real skill's file to edit it recorded a
24
+ * load of that skill. The second is the defect `skill-load-vs-file-edit` fixed
25
+ * on the dashboard side, and it reached grading from here as well as from the
26
+ * hook: `resolveSessionSkillContext` unioned these rows' `skill_name` into the
27
+ * identities a run is graded against. **It no longer does**
28
+ * (`skill-load-source-drift`) — grading reads `skill_load` actions only,
29
+ * because this producer never inspects a `Skill` call's result and so cannot
30
+ * tell an invocation from a permission denial. These rows are cost attribution
31
+ * now, and must not be read back into grading.
32
+ * Restricting to read-shaped tools would not have been enough —
33
+ * opening a skill's file is what editing it starts with. Only an invocation
34
+ * says the instructions were taken as instructions.
35
+ *
36
+ * The cost this pays, deliberately: tokens spent by an agent that followed a
37
+ * `SKILL.md` it opened directly are no longer attributed to that skill. A
38
+ * missing attribution understates a skill's cost; a fabricated one both
39
+ * overstates it and grades a session against a rubric it never invoked.
40
+ *
41
+ * The name is returned exactly as the agent asked for it, so it is *usually*
42
+ * but not always a bare directory segment — `plugin:skill` and `apps/web:deploy`
43
+ * are valid, which `hook-logger.ts:46-52` already accounts for on the other
44
+ * producer. The path-derived branch used to normalize those to a directory
45
+ * name; nothing does now, and the server derives the skill name from the
46
+ * origin as it arrives.
47
+ */
48
+ function detectSkillLoadSignal(message) {
49
+ const content = message.content;
50
+ if (!Array.isArray(content)) {
51
+ return null;
52
+ }
53
+ for (const block of content) {
54
+ const blockRecord = asRecord(block);
55
+ if (!blockRecord || blockRecord.type !== 'tool_use') {
56
+ continue;
57
+ }
58
+ if (blockRecord.name !== 'Skill') {
59
+ continue;
60
+ }
61
+ const input = asRecord(blockRecord.input);
62
+ const skillName = typeof input?.skill === 'string' ? input.skill : null;
63
+ if (skillName) {
64
+ return skillName;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function toNonNegativeInt(value) {
70
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
71
+ ? Math.floor(value)
72
+ : 0;
73
+ }
74
+ /**
75
+ * Reads one transcript's records, degrading to none when the file cannot be
76
+ * read at all.
77
+ *
78
+ * Usage is *optional* data riding a sync whose real job is uploading actions,
79
+ * and since discovery went automatic this runs on every default sync rather
80
+ * than only under `--transcripts-dir`. An unreadable transcript — permissions,
81
+ * or the file being cleaned up between directory discovery and this read —
82
+ * must therefore not reject: an uncaught rejection here aborts the command
83
+ * before `postSyncBatch`, so a transient problem with a token count would take
84
+ * the session's perfectly valid pending actions down with it. Same contract as
85
+ * the cursor file: a failure costs a re-read on the next sync, never a wrong
86
+ * number, because `ingestSessionUsage` replaces with whatever full-session totals
87
+ * the next successful extraction sends.
88
+ */
89
+ async function readJsonlRecords(path) {
90
+ const raw = await readFile(path, 'utf8').catch(() => '');
91
+ return parseJsonlText(raw);
92
+ }
93
+ function parseJsonlText(raw) {
94
+ return raw
95
+ .split('\n')
96
+ .map((line) => line.trim())
97
+ .filter(Boolean)
98
+ .flatMap((line) => {
99
+ try {
100
+ return [JSON.parse(line)];
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ });
106
+ }
107
+ /**
108
+ * Read `[start, end)` and return the records in it, plus how many bytes were
109
+ * actually consumed.
110
+ *
111
+ * Two properties the cursor depends on:
112
+ *
113
+ * - **The read is bounded by `end`, not by EOF.** `end` comes from a `stat`
114
+ * taken before the read, so anything the agent appends while we are reading
115
+ * is simply left for the next sync instead of being consumed by an offset
116
+ * that does not describe it.
117
+ * - **Consumption stops at the last newline.** The Stop hook reads a transcript
118
+ * the agent is actively appending to, so the tail is routinely a half-written
119
+ * line. The scan is done on the `Buffer`, not the decoded string, because a
120
+ * string index counts UTF-16 code units while the offset must be bytes — a
121
+ * single non-ASCII character anywhere in the slice would desynchronise them.
122
+ */
123
+ async function readRecordRange(path, start, end) {
124
+ if (end <= start) {
125
+ return { records: [], consumed: start, ok: true };
126
+ }
127
+ const handle = await open(path, 'r').catch(() => null);
128
+ if (!handle) {
129
+ return { records: [], consumed: start, ok: false };
130
+ }
131
+ try {
132
+ const length = end - start;
133
+ const buffer = Buffer.alloc(length);
134
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
135
+ const slice = buffer.subarray(0, bytesRead);
136
+ const lastNewline = slice.lastIndexOf(0x0a);
137
+ if (lastNewline < 0) {
138
+ return { records: [], consumed: start, ok: true };
139
+ }
140
+ return {
141
+ records: parseJsonlText(slice.subarray(0, lastNewline + 1).toString('utf8')),
142
+ consumed: start + lastNewline + 1,
143
+ ok: true,
144
+ };
145
+ }
146
+ catch {
147
+ return { records: [], consumed: start, ok: false };
148
+ }
149
+ finally {
150
+ await handle.close().catch(() => { });
151
+ }
152
+ }
153
+ /** How many bytes before the cursor identify the consumed prefix. */
154
+ const TAIL_FINGERPRINT_BYTES = 64;
155
+ /**
156
+ * Fingerprint the last bytes of the consumed prefix, or `null` if they cannot
157
+ * be read — which must not be mistaken for "no prefix", since that is exactly
158
+ * the empty-file fingerprint a fresh entry carries.
159
+ */
160
+ async function readTailFingerprint(path, offset) {
161
+ const length = Math.min(TAIL_FINGERPRINT_BYTES, offset);
162
+ if (length === 0) {
163
+ return createHash('sha256').update('').digest('hex').slice(0, 16);
164
+ }
165
+ const handle = await open(path, 'r').catch(() => null);
166
+ if (!handle) {
167
+ return null;
168
+ }
169
+ try {
170
+ const buffer = Buffer.alloc(length);
171
+ const { bytesRead } = await handle.read(buffer, 0, length, offset - length);
172
+ if (bytesRead < length) {
173
+ return null;
174
+ }
175
+ return createHash('sha256').update(buffer).digest('hex').slice(0, 16);
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ finally {
181
+ await handle.close().catch(() => { });
182
+ }
183
+ }
184
+ /**
185
+ * Extracts per-session, per-model token usage from Claude Code JSONL
186
+ * transcripts, plus per-skill
187
+ * Tier-1 "load overhead" usage (§13.3): when an assistant message requests the
188
+ * `Skill` tool, the *next* assistant message's usage is the first-load cost of
189
+ * that skill (v1 approximation — see §13.3's caching nuance). Opening a
190
+ * `SKILL.md` no longer counts, and `detectSkillLoadSignal` says why. Reads only
191
+ * `usage`/`model`/`agentSessionId` and tool_use `name`/`input` off assistant
192
+ * messages — never message text.
193
+ *
194
+ * Each message carries its own (non-cumulative) usage, so we sum every
195
+ * assistant message's usage per (session, model). Messages are deduped by
196
+ * their id so re-reading a transcript that grew since the last sync does not
197
+ * double-count lines already summed.
198
+ *
199
+ * **Dedup is scoped to the session, not the whole extraction.** Claude Code
200
+ * writes one record per *content block* of an assistant message, all sharing a
201
+ * `message.id` and an identical `usage` object, so dropping the repeats is
202
+ * load-bearing — one measured session sums 2.5× too high without it. But the
203
+ * set used to be global across files and was tested *before* the session id was
204
+ * resolved, over lexicographically sorted paths: a message id appearing in two
205
+ * transcripts silently dropped the later session's tokens, with filename sort
206
+ * as the tiebreak. Per-run cost is the only dedup semantics a session-scoped
207
+ * extractor can compute — it never sees the sessions it would have to dedup
208
+ * against — so the account rollup is the sum of these per-run totals, and
209
+ * over-counts by any replayed overlap between sessions.
210
+ *
211
+ * **Given a cache path, only bytes appended since the last call are read.** The
212
+ * fold is a pure left fold over append-only lines, so resuming from a byte
213
+ * offset with the accumulated state produces the totals a cold read would —
214
+ * which matters because `ingestSessionUsage` replaces per `(sessionId, model, source)`
215
+ * with full-session totals, so the totals must still be complete, they just
216
+ * need not be recomputed from the whole file. Every failure mode here degrades
217
+ * to a re-parse rather than a wrong number.
218
+ */
219
+ export async function extractClaudeUsage(files, cachePath) {
220
+ if (!cachePath)
221
+ return foldColdAcrossFiles(files);
222
+ return extractIncrementally(files, cachePath);
223
+ }
224
+ const emptyFoldState = () => ({
225
+ sessionUsage: new Map(),
226
+ sessionSkillUsage: new Map(),
227
+ pendingSkillLoads: new Map(),
228
+ seenMessageKeys: new Set(),
229
+ });
230
+ /** Fold one transcript's records into `state`, in file order. */
231
+ function foldRecords(records, fallbackSessionId, state) {
232
+ for (const recordValue of records) {
233
+ const record = asRecord(recordValue);
234
+ if (!record || record.type !== 'assistant') {
235
+ continue;
236
+ }
237
+ const message = asRecord(record.message);
238
+ if (!message) {
239
+ continue;
240
+ }
241
+ const usage = asRecord(message.usage);
242
+ if (!usage) {
243
+ continue;
244
+ }
245
+ const model = typeof message.model === 'string' ? message.model : null;
246
+ if (!model) {
247
+ continue;
248
+ }
249
+ // Resolve the session *before* the dedup check — the key is scoped to it,
250
+ // and testing first is what let one session's records suppress another's.
251
+ // `record.sessionId` is the harness's own spelling, in a transcript file
252
+ // Mossbear does not write — renaming this key would read a field Claude Code
253
+ // never emits and silently fall back to the file-level hint for every
254
+ // record. Our name for the value is `agentSessionId`; the wire key is not
255
+ // ours to rename.
256
+ const agentSessionId = typeof record.sessionId === 'string' && record.sessionId.length > 0
257
+ ? record.sessionId
258
+ : fallbackSessionId;
259
+ // The `m:`/`u:` prefixes keep a record uuid from colliding with a message
260
+ // id when a record carries no `message.id` to dedup on.
261
+ const messageKey = typeof message.id === 'string'
262
+ ? `${agentSessionId}|m:${message.id}`
263
+ : typeof record.uuid === 'string'
264
+ ? `${agentSessionId}|u:${record.uuid}`
265
+ : null;
266
+ if (messageKey) {
267
+ if (state.seenMessageKeys.has(messageKey)) {
268
+ continue;
269
+ }
270
+ state.seenMessageKeys.add(messageKey);
271
+ }
272
+ const modelMap = state.sessionUsage.get(agentSessionId) ?? new Map();
273
+ state.sessionUsage.set(agentSessionId, modelMap);
274
+ const existing = modelMap.get(model) ?? {
275
+ model,
276
+ provider: inferProviderFromModel(model),
277
+ inputTokens: 0,
278
+ outputTokens: 0,
279
+ cacheReadTokens: 0,
280
+ cacheWriteTokens: 0,
281
+ };
282
+ modelMap.set(model, {
283
+ ...existing,
284
+ inputTokens: existing.inputTokens + toNonNegativeInt(usage.input_tokens),
285
+ outputTokens: existing.outputTokens + toNonNegativeInt(usage.output_tokens),
286
+ cacheReadTokens: (existing.cacheReadTokens ?? 0) + toNonNegativeInt(usage.cache_read_input_tokens),
287
+ cacheWriteTokens: (existing.cacheWriteTokens ?? 0) +
288
+ toNonNegativeInt(usage.cache_creation_input_tokens),
289
+ });
290
+ // Attribute this message's usage to a skill load requested by the
291
+ // *previous* assistant message, if any — this message is the first turn
292
+ // to see that skill's content in context. Must run before detecting a
293
+ // new signal below, so a message that both closes one load and opens
294
+ // another handles both correctly.
295
+ const pendingSkillName = state.pendingSkillLoads.get(agentSessionId);
296
+ if (pendingSkillName) {
297
+ state.pendingSkillLoads.delete(agentSessionId);
298
+ const skillMap = state.sessionSkillUsage.get(agentSessionId) ?? new Map();
299
+ state.sessionSkillUsage.set(agentSessionId, skillMap);
300
+ const skillKey = `${pendingSkillName}::${model}`;
301
+ const existingSkillUsage = skillMap.get(skillKey) ?? {
302
+ skillName: pendingSkillName,
303
+ model,
304
+ inputTokens: 0,
305
+ cacheWriteTokens: 0,
306
+ };
307
+ skillMap.set(skillKey, {
308
+ ...existingSkillUsage,
309
+ inputTokens: existingSkillUsage.inputTokens + toNonNegativeInt(usage.input_tokens),
310
+ cacheWriteTokens: (existingSkillUsage.cacheWriteTokens ?? 0) +
311
+ toNonNegativeInt(usage.cache_creation_input_tokens),
312
+ });
313
+ }
314
+ const skillLoadSignal = detectSkillLoadSignal(message);
315
+ if (skillLoadSignal) {
316
+ state.pendingSkillLoads.set(agentSessionId, skillLoadSignal);
317
+ }
318
+ }
319
+ }
320
+ const fallbackSessionIdFor = (file) =>
321
+ // The session this file was resolved for, when the resolver knew it;
322
+ // otherwise the filename convention both extractors already lean on.
323
+ file.agentSessionId ?? basename(file.path, extname(file.path));
324
+ function toExtracted(state) {
325
+ return {
326
+ runUsage: Array.from(state.sessionUsage.entries()).map(([agentSessionId, modelMap]) => ({
327
+ agentSessionId,
328
+ usage: Array.from(modelMap.values()),
329
+ })),
330
+ skillUsage: Array.from(state.sessionSkillUsage.entries()).map(([agentSessionId, skillMap]) => ({
331
+ agentSessionId,
332
+ usage: Array.from(skillMap.values()),
333
+ })),
334
+ };
335
+ }
336
+ /**
337
+ * Read every file in full and fold them into one shared state — the behaviour
338
+ * that predates the cursor, kept as the answer whenever the cache cannot be
339
+ * used (scan mode, no cache path, or the cross-file session overlap below).
340
+ *
341
+ * The shared `seenMessageKeys` is what makes this distinct from folding each
342
+ * file alone: two transcripts carrying the same session's records dedup against
343
+ * each other here.
344
+ */
345
+ async function foldColdAcrossFiles(files) {
346
+ const state = emptyFoldState();
347
+ for (const file of files) {
348
+ foldRecords(await readJsonlRecords(file.path), fallbackSessionIdFor(file), state);
349
+ }
350
+ return toExtracted(state);
351
+ }
352
+ async function stateToCacheEntry(state, stats, offset, fallbackSessionId, path) {
353
+ const tailHash = await readTailFingerprint(path, offset);
354
+ if (tailHash === null) {
355
+ return null;
356
+ }
357
+ const sessionIds = new Set([
358
+ ...state.sessionUsage.keys(),
359
+ ...state.sessionSkillUsage.keys(),
360
+ ]);
361
+ return {
362
+ size: stats.size,
363
+ mtimeMs: stats.mtimeMs,
364
+ offset,
365
+ sessions: Array.from(sessionIds).map((agentSessionId) => ({
366
+ agentSessionId,
367
+ usage: Array.from(state.sessionUsage.get(agentSessionId)?.values() ?? []),
368
+ skillUsage: Array.from(state.sessionSkillUsage.get(agentSessionId)?.values() ?? []),
369
+ })),
370
+ pendingSkillLoads: Array.from(state.pendingSkillLoads.entries()).map(([agentSessionId, skillName]) => ({ agentSessionId, skillName })),
371
+ seenMessageKeys: Array.from(state.seenMessageKeys),
372
+ tailHash,
373
+ fallbackSessionId,
374
+ };
375
+ }
376
+ function cacheEntryToState(entry) {
377
+ const state = emptyFoldState();
378
+ for (const session of entry.sessions) {
379
+ if (session.usage.length > 0) {
380
+ state.sessionUsage.set(session.agentSessionId, new Map(session.usage.map((usage) => [usage.model, usage])));
381
+ }
382
+ if (session.skillUsage.length > 0) {
383
+ state.sessionSkillUsage.set(session.agentSessionId, new Map(session.skillUsage.map((u) => [`${u.skillName}::${u.model}`, u])));
384
+ }
385
+ }
386
+ for (const pending of entry.pendingSkillLoads) {
387
+ state.pendingSkillLoads.set(pending.agentSessionId, pending.skillName);
388
+ }
389
+ state.seenMessageKeys = new Set(entry.seenMessageKeys);
390
+ return state;
391
+ }
392
+ /**
393
+ * Fold each transcript independently, resuming from the byte offset the last
394
+ * sync stopped at, and merge the per-file results.
395
+ *
396
+ * **Why per-file folding is safe here, and when it is not.** The cold path
397
+ * dedups across files through one shared key set, so two transcripts carrying
398
+ * the same session's records cannot both count it. Per-file state cannot see
399
+ * that, and a cached file's totals were computed without knowledge of its
400
+ * neighbours — there is no way to subtract the overlap after the fact. So the
401
+ * overlap is *detected* and the whole extraction falls back to the cold fold,
402
+ * which is exactly today's behaviour and today's cost. In the ordinary case
403
+ * every file carries one session that no other file carries, and the fallback
404
+ * never fires. Whether a replayed session can carry `usage` at all is the open
405
+ * question slice 0a exists to measure; this degrades correctly either way
406
+ * rather than betting on the answer.
407
+ */
408
+ async function extractIncrementally(files, cachePath) {
409
+ const cache = await readTranscriptUsageCache(cachePath);
410
+ const next = {
411
+ ...emptyTranscriptUsageCache(),
412
+ order: cache.order,
413
+ entries: { ...cache.entries },
414
+ };
415
+ const folded = [];
416
+ for (const file of files) {
417
+ const stats = await stat(file.path).catch(() => null);
418
+ if (!stats?.isFile()) {
419
+ continue;
420
+ }
421
+ const fallbackSessionId = fallbackSessionIdFor(file);
422
+ const entry = cache.entries[file.path];
423
+ // An append leaves the earlier bytes untouched, so the cached fold over
424
+ // them still holds — but only once three things are established: the size
425
+ // did not shrink, the fold was made under the same fallback session, and
426
+ // the consumed prefix is still the same prefix. A larger file is not
427
+ // evidence of an append on its own; the path may have been replaced.
428
+ const candidate = entry &&
429
+ entry.fallbackSessionId === fallbackSessionId &&
430
+ stats.size >= entry.size &&
431
+ stats.size >= entry.offset;
432
+ const reusable = candidate && (await readTailFingerprint(file.path, entry.offset)) === entry.tailHash;
433
+ // Same size *and* mtime means nothing was appended, so there is nothing to
434
+ // read at all.
435
+ const unchanged = reusable && stats.size === entry.size && stats.mtimeMs === entry.mtimeMs;
436
+ let state;
437
+ let offset;
438
+ let readOk = true;
439
+ if (unchanged) {
440
+ state = cacheEntryToState(entry);
441
+ offset = entry.offset;
442
+ }
443
+ else if (reusable) {
444
+ state = cacheEntryToState(entry);
445
+ const read = await readRecordRange(file.path, entry.offset, stats.size);
446
+ foldRecords(read.records, fallbackSessionId, state);
447
+ offset = read.consumed;
448
+ readOk = read.ok;
449
+ }
450
+ else {
451
+ state = emptyFoldState();
452
+ const read = await readRecordRange(file.path, 0, stats.size);
453
+ foldRecords(read.records, fallbackSessionId, state);
454
+ offset = read.consumed;
455
+ readOk = read.ok;
456
+ }
457
+ folded.push(state);
458
+ if (readOk) {
459
+ const written = await stateToCacheEntry(state, stats, offset, fallbackSessionId, file.path);
460
+ if (written) {
461
+ next.entries[file.path] = written;
462
+ next.order = touchCacheOrder(next.order, file.path);
463
+ }
464
+ }
465
+ // A failed read leaves the previous entry alone. Recording this `stat` as
466
+ // successfully folded would let the `unchanged` branch trust an empty fold
467
+ // for as long as the file does not change — a permanent silent zero for
468
+ // that session, which is the failure this whole item exists to remove.
469
+ }
470
+ const seenSessions = new Set();
471
+ for (const state of folded) {
472
+ for (const agentSessionId of new Set([
473
+ ...state.sessionUsage.keys(),
474
+ ...state.sessionSkillUsage.keys(),
475
+ ])) {
476
+ if (seenSessions.has(agentSessionId)) {
477
+ // Two transcripts claim one session — see the doc comment. Leave the
478
+ // cache as it was: the per-file entries are still individually valid,
479
+ // they just cannot be summed.
480
+ return foldColdAcrossFiles(files);
481
+ }
482
+ seenSessions.add(agentSessionId);
483
+ }
484
+ }
485
+ await writeTranscriptUsageCache(cachePath, next);
486
+ const merged = emptyFoldState();
487
+ for (const state of folded) {
488
+ for (const [agentSessionId, modelMap] of state.sessionUsage) {
489
+ merged.sessionUsage.set(agentSessionId, modelMap);
490
+ }
491
+ for (const [agentSessionId, skillMap] of state.sessionSkillUsage) {
492
+ merged.sessionSkillUsage.set(agentSessionId, skillMap);
493
+ }
494
+ }
495
+ return toExtracted(merged);
496
+ }
497
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/usage/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { ExtractedSessionUsage, ResolvedTranscript } from './types.js';
2
+ /** Extracts per-response usage from either Codex rollout format, once per turn. */
3
+ export declare function extractCodexUsage(files: readonly ResolvedTranscript[]): Promise<ExtractedSessionUsage>;
4
+ //# sourceMappingURL=codex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/usage/codex.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAgB,MAAM,YAAY,CAAA;AA6FzF,mFAAmF;AACnF,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,SAAS,kBAAkB,EAAE,GACnC,OAAO,CAAC,qBAAqB,CAAC,CAoEhC"}