sidekick-shared 0.27.2 → 0.27.3

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +24 -3
  3. package/dist/aggregation/EventAggregator.d.ts +12 -2
  4. package/dist/aggregation/EventAggregator.js +49 -19
  5. package/dist/browser.d.ts +2 -1
  6. package/dist/browser.js +9 -2
  7. package/dist/extractors/tokenUsage.js +1 -0
  8. package/dist/formatters/sessionDump.d.ts +10 -2
  9. package/dist/formatters/sessionDump.js +45 -23
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.js +31 -10
  12. package/dist/node.d.ts +2 -0
  13. package/dist/node.js +8 -2
  14. package/dist/parsers/codexParser.d.ts +22 -2
  15. package/dist/parsers/codexParser.js +64 -5
  16. package/dist/parsers/subagentScanner.d.ts +9 -0
  17. package/dist/parsers/subagentScanner.js +45 -12
  18. package/dist/parsers/subagentStatsCache.d.ts +17 -0
  19. package/dist/parsers/subagentStatsCache.js +82 -0
  20. package/dist/parsers/subagentTraceParser.js +19 -10
  21. package/dist/providers/claudeCode.js +8 -41
  22. package/dist/providers/codex.js +28 -14
  23. package/dist/providers/codexDatabase.d.ts +16 -1
  24. package/dist/providers/codexDatabase.js +49 -1
  25. package/dist/providers/openCode.js +30 -4
  26. package/dist/report/htmlReportGenerator.js +33 -11
  27. package/dist/report/sessionReportInputs.d.ts +3 -1
  28. package/dist/report/sessionReportInputs.js +2 -0
  29. package/dist/report/types.d.ts +5 -0
  30. package/dist/schemas/sessionEvent.d.ts +30 -6
  31. package/dist/schemas/sessionEvent.js +7 -1
  32. package/dist/schemas/stateFile.d.ts +2 -0
  33. package/dist/schemas/stateFile.js +1 -0
  34. package/dist/sessionTokenTotals.d.ts +46 -0
  35. package/dist/sessionTokenTotals.js +138 -0
  36. package/dist/stateFile.d.ts +7 -0
  37. package/dist/tokenSummary.d.ts +27 -1
  38. package/dist/tokenSummary.js +50 -3
  39. package/dist/types/codex.d.ts +4 -0
  40. package/dist/types/sessionEvent.d.ts +22 -0
  41. package/dist/usage/claudeUsageDedupe.d.ts +53 -0
  42. package/dist/usage/claudeUsageDedupe.js +195 -0
  43. package/dist/usage/subagentUsage.d.ts +13 -0
  44. package/dist/usage/subagentUsage.js +21 -0
  45. package/dist/usage/usageEvents.d.ts +11 -2
  46. package/dist/usage/usageEvents.js +79 -8
  47. package/dist/watchers/eventBridge.js +5 -0
  48. package/dist/watchers/jsonlWatcher.d.ts +5 -0
  49. package/dist/watchers/jsonlWatcher.js +40 -5
  50. package/dist/watchers/types.d.ts +5 -0
  51. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Accumulate normalized usage into a subagent's token fields so every
3
+ * provider's `scanSubagents()` reports the same four buckets and the same
4
+ * cache-inclusive total as `summarizeTokens()`.
5
+ *
6
+ * Browser-safe: no Node imports.
7
+ *
8
+ * @module usage/subagentUsage
9
+ */
10
+ import type { NormalizedUsage } from '../usageNormalization';
11
+ import type { SubagentStats } from '../types/sessionEvent';
12
+ /** Add one usage record (a call or a correction) to a subagent's token totals. */
13
+ export declare function addUsageToSubagent(stats: SubagentStats, usage: NormalizedUsage): void;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Accumulate normalized usage into a subagent's token fields so every
4
+ * provider's `scanSubagents()` reports the same four buckets and the same
5
+ * cache-inclusive total as `summarizeTokens()`.
6
+ *
7
+ * Browser-safe: no Node imports.
8
+ *
9
+ * @module usage/subagentUsage
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.addUsageToSubagent = addUsageToSubagent;
13
+ /** Add one usage record (a call or a correction) to a subagent's token totals. */
14
+ function addUsageToSubagent(stats, usage) {
15
+ stats.inputTokens += usage.uncachedInputTokens;
16
+ stats.outputTokens += usage.outputTokens;
17
+ stats.cacheReadTokens = (stats.cacheReadTokens ?? 0) + usage.cacheReadTokens;
18
+ stats.cacheWriteTokens = (stats.cacheWriteTokens ?? 0) + usage.cacheWriteTokens;
19
+ stats.reasoningTokens = (stats.reasoningTokens ?? 0) + usage.reasoningTokens;
20
+ stats.totalTokens = (stats.totalTokens ?? 0) + usage.totalTokens;
21
+ }
@@ -10,6 +10,11 @@
10
10
  * catalog refresh never leaves stale costs behind. Cache files are touched on
11
11
  * every hit and pruned least-recently-used, once per collection.
12
12
  *
13
+ * A Claude Code session's subagents write their own transcripts under
14
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session
15
+ * and their sizes and mtimes join the fingerprint. (Codex spawned subagents
16
+ * are top-level rollouts, already listed as sessions of their own.)
17
+ *
13
18
  * Node-only: reads session files and the cache directory.
14
19
  *
15
20
  * @module usage/usageEvents
@@ -17,8 +22,12 @@
17
22
  import type { ProviderId, SessionProviderBase, SessionProviderDiagnostic } from '../providers/types';
18
23
  import type { TokenTotals } from '../types/historicalData';
19
24
  import type { PricingProvenance } from '../usageNormalization';
20
- /** Bump when the cached record shape changes; older files are re-read. */
21
- export declare const USAGE_CACHE_VERSION = 1;
25
+ /**
26
+ * Bump when the cached record shape or the counting rules change; older files
27
+ * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
28
+ * are counted once, and Claude subagent transcripts are included.
29
+ */
30
+ export declare const USAGE_CACHE_VERSION = 2;
22
31
  /**
23
32
  * Most-recently-used cache files kept on disk. Sized for a busy month of
24
33
  * sessions (files are a few KB each); a collection larger than this still
@@ -11,6 +11,11 @@
11
11
  * catalog refresh never leaves stale costs behind. Cache files are touched on
12
12
  * every hit and pruned least-recently-used, once per collection.
13
13
  *
14
+ * A Claude Code session's subagents write their own transcripts under
15
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session
16
+ * and their sizes and mtimes join the fingerprint. (Codex spawned subagents
17
+ * are top-level rollouts, already listed as sessions of their own.)
18
+ *
14
19
  * Node-only: reads session files and the cache directory.
15
20
  *
16
21
  * @module usage/usageEvents
@@ -59,8 +64,12 @@ const paths_1 = require("../paths");
59
64
  const sessionPreviews_1 = require("../sessionPreviews");
60
65
  const usageNormalization_1 = require("../usageNormalization");
61
66
  const atomic_1 = require("../writers/atomic");
62
- /** Bump when the cached record shape changes; older files are re-read. */
63
- exports.USAGE_CACHE_VERSION = 1;
67
+ /**
68
+ * Bump when the cached record shape or the counting rules change; older files
69
+ * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
70
+ * are counted once, and Claude subagent transcripts are included.
71
+ */
72
+ exports.USAGE_CACHE_VERSION = 2;
64
73
  /**
65
74
  * Most-recently-used cache files kept on disk. Sized for a busy month of
66
75
  * sessions (files are a few KB each); a collection larger than this still
@@ -188,20 +197,81 @@ function priceCachedEvent(cached, provider, sessionId, project) {
188
197
  costProvenance: priced.source,
189
198
  };
190
199
  }
191
- function readSessionUsage(provider, preview) {
192
- const reader = provider.createReader(preview.filePath);
200
+ const CLAUDE_AGENT_FILE = /^agent-.+\.jsonl$/;
201
+ /** Claude Code subagent transcripts of a session, sorted by name. */
202
+ function claudeSubagentFiles(preview) {
203
+ if (preview.provider !== 'claude-code')
204
+ return [];
205
+ const dir = path.join(path.dirname(preview.filePath), preview.sessionId, 'subagents');
206
+ let names;
207
+ try {
208
+ names = fs.readdirSync(dir).filter((name) => CLAUDE_AGENT_FILE.test(name));
209
+ }
210
+ catch {
211
+ return [];
212
+ }
213
+ const files = [];
214
+ for (const name of names.sort()) {
215
+ try {
216
+ const stat = fs.statSync(path.join(dir, name));
217
+ files.push({
218
+ path: path.join(dir, name),
219
+ name,
220
+ sizeBytes: stat.size,
221
+ modifiedAt: stat.mtimeMs,
222
+ });
223
+ }
224
+ catch {
225
+ // Removed between listing and stat; the next collection re-reads.
226
+ }
227
+ }
228
+ return files;
229
+ }
230
+ function sessionFingerprint(preview, subagents) {
231
+ const base = `${preview.sizeBytes}:${preview.modifiedAt}`;
232
+ if (subagents.length === 0)
233
+ return base;
234
+ return `${base}|${subagents.map((f) => `${f.name}:${f.sizeBytes}:${f.modifiedAt}`).join(',')}`;
235
+ }
236
+ function readTranscriptUsage(provider, filePath, cached) {
237
+ const reader = provider.createReader(filePath);
193
238
  const events = reader.readAll();
194
239
  reader.flush();
195
- const cached = [];
240
+ // A correction tops up the call its message already recorded, so it merges
241
+ // into that record instead of counting as another call.
242
+ const byMessageId = new Map();
196
243
  for (const event of events) {
197
244
  const usage = (0, usageNormalization_1.extractNormalizedUsage)(event);
198
245
  if (!usage)
199
246
  continue;
247
+ const messageId = event.message?.id;
248
+ const target = event.message?.usageKind === 'correction' && messageId
249
+ ? byMessageId.get(messageId)
250
+ : undefined;
251
+ if (target) {
252
+ target.i += usage.uncachedInputTokens;
253
+ target.o += usage.outputTokens;
254
+ target.cw += usage.cacheWriteTokens;
255
+ target.cr += usage.cacheReadTokens;
256
+ target.bo += usage.billableOutputTokens;
257
+ target.tt += usage.totalTokens;
258
+ continue;
259
+ }
200
260
  const timestamp = Date.parse(event.timestamp);
201
261
  if (!Number.isFinite(timestamp))
202
262
  continue;
203
- cached.push(cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown'));
263
+ const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown');
264
+ cached.push(record);
265
+ if (messageId)
266
+ byMessageId.set(messageId, record);
204
267
  }
268
+ }
269
+ function readSessionUsage(provider, preview, subagents) {
270
+ const cached = [];
271
+ readTranscriptUsage(provider, preview.filePath, cached);
272
+ for (const file of subagents)
273
+ readTranscriptUsage(provider, file.path, cached);
274
+ cached.sort((a, b) => a.t - b.t);
205
275
  return cached;
206
276
  }
207
277
  function yieldToEventLoop() {
@@ -239,7 +309,8 @@ async function collectUsageEvents(options) {
239
309
  const provider = providerById.get(preview.provider);
240
310
  if (!provider)
241
311
  continue;
242
- const fingerprint = `${preview.sizeBytes}:${preview.modifiedAt}`;
312
+ const subagentFiles = claudeSubagentFiles(preview);
313
+ const fingerprint = sessionFingerprint(preview, subagentFiles);
243
314
  const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
244
315
  let cachedEvents = null;
245
316
  let project = preview.workspacePath;
@@ -257,7 +328,7 @@ async function collectUsageEvents(options) {
257
328
  if (!cachedEvents) {
258
329
  cacheMisses += 1;
259
330
  try {
260
- cachedEvents = readSessionUsage(provider, preview);
331
+ cachedEvents = readSessionUsage(provider, preview, subagentFiles);
261
332
  }
262
333
  catch (error) {
263
334
  const diagnostic = {
@@ -29,6 +29,7 @@ function toFollowEvents(event, providerId) {
29
29
  ? { read: usage.cache_read_input_tokens || 0, write: usage.cache_creation_input_tokens || 0 }
30
30
  : undefined;
31
31
  const cost = usage?.reported_cost;
32
+ const usageKind = usage ? event.message?.usageKind : undefined;
32
33
  const model = event.message?.model;
33
34
  const rateLimits = event.rateLimits;
34
35
  switch (event.type) {
@@ -94,6 +95,7 @@ function toFollowEvents(event, providerId) {
94
95
  summary: text || '(thinking...)',
95
96
  tokens,
96
97
  cacheTokens,
98
+ ...(usageKind ? { usageKind } : {}),
97
99
  cost,
98
100
  model,
99
101
  raw: event,
@@ -104,6 +106,8 @@ function toFollowEvents(event, providerId) {
104
106
  const last = events[events.length - 1];
105
107
  last.tokens = tokens;
106
108
  last.cacheTokens = cacheTokens;
109
+ if (usageKind)
110
+ last.usageKind = usageKind;
107
111
  last.cost = cost;
108
112
  }
109
113
  break;
@@ -158,6 +162,7 @@ function toFollowEvents(event, providerId) {
158
162
  summary: label && text ? `${label}: ${text}` : summary,
159
163
  tokens,
160
164
  cacheTokens,
165
+ ...(usageKind ? { usageKind } : {}),
161
166
  cost,
162
167
  model,
163
168
  rateLimits,
@@ -11,6 +11,9 @@ export declare class JsonlSessionWatcher implements SessionWatcher {
11
11
  private tail;
12
12
  private startOffset;
13
13
  private codexInPlanMode;
14
+ private readonly claudeUsageDeduper;
15
+ /** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
16
+ private codexPrevTotal;
14
17
  constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks);
15
18
  get isActive(): boolean;
16
19
  /** Seek to a committed byte offset before starting replay. */
@@ -20,5 +23,7 @@ export declare class JsonlSessionWatcher implements SessionWatcher {
20
23
  start(replay: boolean): void;
21
24
  stop(): void;
22
25
  private handleRawEvent;
26
+ /** One call's usage from a token_count `info`, skipping duplicate events. */
27
+ private codexCallUsage;
23
28
  private normalizeCodexEvent;
24
29
  }
@@ -8,12 +8,29 @@ exports.JsonlSessionWatcher = void 0;
8
8
  const jsonlTail_1 = require("./jsonlTail");
9
9
  const codexParser_1 = require("../parsers/codexParser");
10
10
  const toolSummary_1 = require("../formatters/toolSummary");
11
+ const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
11
12
  // ── Normalizers ──
12
- function normalizeClaudeCodeEvent(raw) {
13
+ /**
14
+ * Split lines of one Claude response repeat its usage; run the raw usage
15
+ * through the shared deduper so each response is counted once (last copy wins).
16
+ */
17
+ function dedupedClaudeUsage(raw, deduper) {
18
+ if (!raw.message?.usage)
19
+ return { usage: undefined, usageKind: undefined };
20
+ const event = deduper.apply((0, claudeUsageDedupe_1.normalizeClaudeUsage)({
21
+ type: 'assistant',
22
+ timestamp: raw.timestamp || '',
23
+ message: raw.message,
24
+ }));
25
+ return { usage: event.message?.usage, usageKind: event.message?.usageKind };
26
+ }
27
+ function normalizeClaudeCodeEvent(raw, deduper) {
13
28
  const events = [];
14
29
  const ts = raw.timestamp || new Date().toISOString();
15
30
  const permissionMode = raw.permissionMode;
16
- const usage = raw.message?.usage;
31
+ const { usage, usageKind } = raw.type === 'assistant'
32
+ ? dedupedClaudeUsage(raw, deduper)
33
+ : { usage: undefined, usageKind: undefined };
17
34
  const tokens = usage
18
35
  ? { input: usage.input_tokens || 0, output: usage.output_tokens || 0 }
19
36
  : undefined;
@@ -83,6 +100,7 @@ function normalizeClaudeCodeEvent(raw) {
83
100
  summary: text || '(thinking...)',
84
101
  tokens,
85
102
  cacheTokens,
103
+ ...(usageKind ? { usageKind } : {}),
86
104
  cost,
87
105
  model,
88
106
  raw,
@@ -93,6 +111,8 @@ function normalizeClaudeCodeEvent(raw) {
93
111
  const last = events[events.length - 1];
94
112
  last.tokens = tokens;
95
113
  last.cacheTokens = cacheTokens;
114
+ if (usageKind)
115
+ last.usageKind = usageKind;
96
116
  last.cost = cost;
97
117
  }
98
118
  }
@@ -190,6 +210,9 @@ class JsonlSessionWatcher {
190
210
  tail;
191
211
  startOffset;
192
212
  codexInPlanMode = false;
213
+ claudeUsageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
214
+ /** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
215
+ codexPrevTotal;
193
216
  constructor(providerId, sessionPath, callbacks) {
194
217
  this.providerId = providerId;
195
218
  this.sessionPath = sessionPath;
@@ -223,17 +246,25 @@ class JsonlSessionWatcher {
223
246
  stop() {
224
247
  this.tail?.stop();
225
248
  this.codexInPlanMode = false;
249
+ this.claudeUsageDeduper.reset();
250
+ this.codexPrevTotal = undefined;
226
251
  }
227
252
  handleRawEvent(event) {
228
253
  // For codex, raw events don't match RawSessionEvent shape exactly, but
229
254
  // JsonlParser parses any JSON object. Cast to Record for codex normalizer.
230
255
  const followEvents = this.providerId === 'codex'
231
256
  ? this.normalizeCodexEvent(event)
232
- : normalizeClaudeCodeEvent(event);
257
+ : normalizeClaudeCodeEvent(event, this.claudeUsageDeduper);
233
258
  for (const fe of followEvents) {
234
259
  this.callbacks.onEvent(fe);
235
260
  }
236
261
  }
262
+ /** One call's usage from a token_count `info`, skipping duplicate events. */
263
+ codexCallUsage(info) {
264
+ const resolved = (0, codexParser_1.resolveCodexCallUsage)(info, this.codexPrevTotal ?? null);
265
+ this.codexPrevTotal = resolved.total ?? undefined;
266
+ return resolved.usage;
267
+ }
237
268
  normalizeCodexEvent(raw) {
238
269
  const events = [];
239
270
  const ts = raw.timestamp || new Date().toISOString();
@@ -332,7 +363,7 @@ class JsonlSessionWatcher {
332
363
  const evtType = payload?.type;
333
364
  if (evtType === 'token_count') {
334
365
  const info = payload?.info;
335
- const usage = (info?.last_token_usage || info?.total_token_usage);
366
+ const usage = this.codexCallUsage(info);
336
367
  const rl = payload?.rate_limits;
337
368
  const rateLimits = rl ? extractRateLimits(rl) : undefined;
338
369
  if (usage || rateLimits) {
@@ -343,12 +374,16 @@ class JsonlSessionWatcher {
343
374
  summary: usage
344
375
  ? `Tokens: ${usage.input_tokens ?? 0} in / ${usage.output_tokens ?? 0} out`
345
376
  : 'Rate limits updated',
377
+ // Codex input includes cached input; FollowEvent buckets are disjoint.
346
378
  tokens: usage
347
379
  ? {
348
- input: usage.input_tokens || 0,
380
+ input: Math.max(0, (usage.input_tokens || 0) - (usage.cached_input_tokens || 0)),
349
381
  output: usage.output_tokens || 0,
350
382
  }
351
383
  : undefined,
384
+ cacheTokens: usage?.cached_input_tokens
385
+ ? { read: usage.cached_input_tokens, write: 0 }
386
+ : undefined,
352
387
  rateLimits,
353
388
  raw,
354
389
  });
@@ -18,6 +18,11 @@ export interface FollowEvent {
18
18
  read: number;
19
19
  write: number;
20
20
  };
21
+ /**
22
+ * `'correction'` marks tokens that top up a response already counted (a later
23
+ * split line reported more). Absent means a regular call.
24
+ */
25
+ usageKind?: 'call' | 'correction';
21
26
  cost?: number;
22
27
  toolName?: string;
23
28
  toolInput?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.27.2",
3
+ "version": "0.27.3",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "keywords": [
6
6
  "claude-code",