sidekick-shared 0.28.3 → 0.29.1

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 (151) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +188 -27
  3. package/dist/aggregation/EventAggregator.d.ts +19 -0
  4. package/dist/aggregation/EventAggregator.js +174 -35
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +13 -0
  8. package/dist/authStatus.d.ts +54 -0
  9. package/dist/authStatus.js +91 -0
  10. package/dist/browser.d.ts +16 -6
  11. package/dist/browser.js +24 -2
  12. package/dist/codex.d.ts +23 -0
  13. package/dist/codex.js +44 -0
  14. package/dist/codexProfiles.d.ts +1 -0
  15. package/dist/codexProfiles.js +8 -11
  16. package/dist/context/sessionContext.js +4 -0
  17. package/dist/esm/aggregation/EventAggregator.js +174 -35
  18. package/dist/esm/aggregation/snapshot.js +31 -9
  19. package/dist/esm/authStatus.js +87 -0
  20. package/dist/esm/browser.js +5 -2
  21. package/dist/esm/codex.js +18 -0
  22. package/dist/esm/codexProfiles.js +8 -11
  23. package/dist/esm/context/sessionContext.js +4 -0
  24. package/dist/esm/formatters/toolSummary.js +4 -1
  25. package/dist/esm/formatting.js +10 -1
  26. package/dist/esm/index.js +11 -6
  27. package/dist/esm/modelCatalog.js +7 -5
  28. package/dist/esm/modelContext.js +4 -3
  29. package/dist/esm/modelInfo.js +425 -47
  30. package/dist/esm/node.js +9 -3
  31. package/dist/esm/observedContextWindows.js +2 -1
  32. package/dist/esm/ownKey.js +26 -0
  33. package/dist/esm/parsers/codexParser.js +351 -54
  34. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  35. package/dist/esm/parsers/openCodeParser.js +85 -22
  36. package/dist/esm/parsers/subagentScanner.js +33 -12
  37. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  38. package/dist/esm/pricing.js +18 -0
  39. package/dist/esm/pricingCatalog.js +17 -9
  40. package/dist/esm/promptHistory.js +66 -4
  41. package/dist/esm/providerFailure.js +124 -3
  42. package/dist/esm/providers/claudeCode.js +10 -2
  43. package/dist/esm/providers/codex.js +84 -62
  44. package/dist/esm/providers/codexDatabase.js +3 -19
  45. package/dist/esm/providers/codexRollouts.js +24 -1
  46. package/dist/esm/providers/openCode.js +9 -17
  47. package/dist/esm/report/htmlReportGenerator.js +42 -5
  48. package/dist/esm/report/transcriptParser.js +13 -11
  49. package/dist/esm/schemas/providerFailure.js +3 -1
  50. package/dist/esm/schemas/sessionEvent.js +35 -14
  51. package/dist/esm/schemas/transcript.js +4 -0
  52. package/dist/esm/sessionPromptHistory.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/subagentSpawn.js +163 -0
  55. package/dist/esm/testing/credentialGuard.js +87 -5
  56. package/dist/esm/toolCallOrigins.js +310 -0
  57. package/dist/esm/transcript.js +46 -3
  58. package/dist/esm/turns/assistantTurn.js +3 -2
  59. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  60. package/dist/esm/usage/usageEvents.js +146 -17
  61. package/dist/esm/usage/usageReports.js +39 -6
  62. package/dist/esm/watchers/eventBridge.js +15 -0
  63. package/dist/esm/watchers/factory.js +5 -3
  64. package/dist/esm/watchers/jsonlTail.js +442 -58
  65. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  66. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  67. package/dist/esm/zaiQuota.js +2 -1
  68. package/dist/formatters/toolSummary.js +4 -1
  69. package/dist/formatting.d.ts +26 -1
  70. package/dist/formatting.js +11 -1
  71. package/dist/index.d.ts +22 -11
  72. package/dist/index.js +44 -12
  73. package/dist/modelCatalog.js +7 -5
  74. package/dist/modelContext.js +4 -3
  75. package/dist/modelInfo.d.ts +185 -16
  76. package/dist/modelInfo.js +434 -49
  77. package/dist/node.d.ts +16 -5
  78. package/dist/node.js +33 -2
  79. package/dist/observedContextWindows.js +2 -1
  80. package/dist/ownKey.d.ts +16 -0
  81. package/dist/ownKey.js +30 -0
  82. package/dist/parsers/codexParser.d.ts +118 -0
  83. package/dist/parsers/codexParser.js +352 -54
  84. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  85. package/dist/parsers/codexRolloutOrigin.js +111 -0
  86. package/dist/parsers/openCodeParser.d.ts +38 -0
  87. package/dist/parsers/openCodeParser.js +88 -22
  88. package/dist/parsers/subagentScanner.d.ts +19 -6
  89. package/dist/parsers/subagentScanner.js +33 -12
  90. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  91. package/dist/parsers/subagentTraceParser.js +97 -37
  92. package/dist/pricing.d.ts +22 -0
  93. package/dist/pricing.js +50 -0
  94. package/dist/pricingCatalog.d.ts +10 -0
  95. package/dist/pricingCatalog.js +16 -8
  96. package/dist/promptHistory.d.ts +65 -4
  97. package/dist/promptHistory.js +68 -5
  98. package/dist/providerFailure.d.ts +44 -0
  99. package/dist/providerFailure.js +126 -4
  100. package/dist/providers/claudeCode.js +9 -1
  101. package/dist/providers/codex.d.ts +3 -22
  102. package/dist/providers/codex.js +85 -64
  103. package/dist/providers/codexDatabase.d.ts +3 -2
  104. package/dist/providers/codexDatabase.js +4 -22
  105. package/dist/providers/codexRollouts.d.ts +22 -0
  106. package/dist/providers/codexRollouts.js +24 -0
  107. package/dist/providers/openCode.js +8 -16
  108. package/dist/providers/types.d.ts +14 -0
  109. package/dist/report/htmlReportGenerator.js +42 -5
  110. package/dist/report/transcriptParser.d.ts +10 -1
  111. package/dist/report/transcriptParser.js +13 -11
  112. package/dist/schemas/index.d.ts +1 -0
  113. package/dist/schemas/providerFailure.d.ts +39 -0
  114. package/dist/schemas/providerFailure.js +2 -0
  115. package/dist/schemas/sessionEvent.d.ts +27 -10
  116. package/dist/schemas/sessionEvent.js +35 -14
  117. package/dist/schemas/transcript.d.ts +2 -0
  118. package/dist/schemas/transcript.js +4 -0
  119. package/dist/sessionPromptHistory.d.ts +7 -1
  120. package/dist/sessionPromptHistory.js +1 -1
  121. package/dist/statusline/payload.d.ts +10 -0
  122. package/dist/statusline/payload.js +14 -0
  123. package/dist/subagentSpawn.d.ts +80 -0
  124. package/dist/subagentSpawn.js +174 -0
  125. package/dist/testing/credentialGuard.d.ts +14 -0
  126. package/dist/testing/credentialGuard.js +87 -5
  127. package/dist/toolCallOrigins.d.ts +70 -0
  128. package/dist/toolCallOrigins.js +346 -0
  129. package/dist/transcript.d.ts +12 -0
  130. package/dist/transcript.js +46 -3
  131. package/dist/turns/assistantTurn.d.ts +1 -0
  132. package/dist/turns/assistantTurn.js +3 -2
  133. package/dist/types/opencode.d.ts +2 -0
  134. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  135. package/dist/usage/claudeUsageDedupe.js +68 -0
  136. package/dist/usage/usageEvents.d.ts +54 -6
  137. package/dist/usage/usageEvents.js +146 -17
  138. package/dist/usage/usageReports.d.ts +35 -4
  139. package/dist/usage/usageReports.js +39 -6
  140. package/dist/watchers/eventBridge.js +15 -0
  141. package/dist/watchers/factory.d.ts +6 -0
  142. package/dist/watchers/factory.js +5 -3
  143. package/dist/watchers/jsonlTail.d.ts +124 -4
  144. package/dist/watchers/jsonlTail.js +446 -59
  145. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  146. package/dist/watchers/jsonlWatcher.js +50 -3
  147. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  148. package/dist/watchers/providerReaderWatcher.js +13 -0
  149. package/dist/watchers/types.d.ts +12 -0
  150. package/dist/zaiQuota.js +2 -1
  151. package/package.json +41 -1
@@ -6,12 +6,12 @@ exports.segmentAssistantTurn = segmentAssistantTurn;
6
6
  exports.assistantTurnEventsFromSessionEvents = assistantTurnEventsFromSessionEvents;
7
7
  exports.extractTurnSubagents = extractTurnSubagents;
8
8
  const toolSummary_1 = require("../formatters/toolSummary");
9
+ const subagentSpawn_1 = require("../subagentSpawn");
9
10
  const DEFAULT_NARRATION_CHARS = 4000;
10
11
  const DEFAULT_REASONING_CHARS = 8000;
11
12
  const DEFAULT_PROCESS_STEPS = 60;
12
13
  const DEFAULT_REASONING_BLOCKS = 24;
13
14
  const DEFAULT_TOOL_INPUT_CHARS = 200;
14
- const SUBAGENT_TOOL_NAMES = new Set(['Task']);
15
15
  const LABEL_MAX = 80;
16
16
  const TITLE_RE = /^\s*\*\*([^*\n]+)\*\*[ \t]*(?:\n+([\s\S]*))?$/;
17
17
  const TITLE_MAX = 100;
@@ -29,8 +29,9 @@ function reasoningSummary(text) {
29
29
  }
30
30
  return { title: null, body: trimmed };
31
31
  }
32
+ /** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
32
33
  function isAssistantTurnSubagentTool(toolName) {
33
- return toolName != null && SUBAGENT_TOOL_NAMES.has(toolName);
34
+ return (0, subagentSpawn_1.isSubagentSpawnTool)(toolName);
34
35
  }
35
36
  function segmentAssistantTurn(events, options = {}) {
36
37
  const maxNarrationChars = options.maxNarrationChars ?? DEFAULT_NARRATION_CHARS;
@@ -42,6 +42,8 @@ export interface OpenCodeMessage {
42
42
  cacheRead?: number;
43
43
  cacheWrite?: number;
44
44
  reasoning?: number;
45
+ /** Provider-reported total; tells OpenCode's token layouts apart (see `normalizeOpenCodeUsage`). */
46
+ total?: number;
45
47
  };
46
48
  time: {
47
49
  created: string | number;
@@ -22,6 +22,24 @@ import type { SessionEvent } from '../types/sessionEvent';
22
22
  export declare const SYNTHETIC_MODEL = "<synthetic>";
23
23
  /** Attach Anthropic-semantics normalized usage and Claude Code provenance to an event. */
24
24
  export declare function normalizeClaudeUsage(event: SessionEvent): SessionEvent;
25
+ /** Tokens a {@link ClaudeUsageDeduper} has already counted for one message id. */
26
+ export interface ClaudeUsageDedupeBuckets {
27
+ input: number;
28
+ cacheRead: number;
29
+ cacheWrite: number;
30
+ output: number;
31
+ reasoning: number;
32
+ }
33
+ /**
34
+ * JSON-serializable state of a {@link ClaudeUsageDeduper}, from
35
+ * `exportState()`, for `importState()` in a reader resumed at the same
36
+ * position (for example from a persisted session snapshot).
37
+ */
38
+ export interface ClaudeUsageDedupeState {
39
+ version: 1;
40
+ /** `[message id, counted tokens]` pairs, oldest first. */
41
+ entries: Array<[string, ClaudeUsageDedupeBuckets]>;
42
+ }
25
43
  /**
26
44
  * Per-reader dedupe state. Split lines of one response are adjacent, so a
27
45
  * bounded map (oldest keys evicted first) is enough.
@@ -40,6 +58,19 @@ export declare class ClaudeUsageDeduper {
40
58
  /** Whether a message id has already been counted by this deduper. */
41
59
  has(id: string): boolean;
42
60
  reset(): void;
61
+ /**
62
+ * The most recent `limit` message ids and the tokens counted for each,
63
+ * oldest first. Split lines of one response are adjacent, so a reader
64
+ * resumed at the position this was taken at needs only the last few.
65
+ */
66
+ exportState(limit?: number): ClaudeUsageDedupeState;
67
+ /**
68
+ * Replaces this deduper's state with one from {@link exportState}. Returns
69
+ * false, changing nothing, when `state` is not a valid version-1 state
70
+ * (wrong shape, empty ids, or counts that are not finite non-negative
71
+ * numbers). Entries beyond this deduper's capacity drop oldest first.
72
+ */
73
+ importState(state: unknown): boolean;
43
74
  private remember;
44
75
  }
45
76
  /**
@@ -64,6 +64,25 @@ function normalizeClaudeUsage(event) {
64
64
  },
65
65
  };
66
66
  }
67
+ const BUCKET_KEYS = ['input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'];
68
+ /** Entries {@link ClaudeUsageDeduper.exportState} keeps by default. */
69
+ const DEFAULT_EXPORT_LIMIT = 256;
70
+ function isCount(value) {
71
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
72
+ }
73
+ function bucketsFrom(value) {
74
+ if (!value || typeof value !== 'object' || Array.isArray(value))
75
+ return null;
76
+ const record = value;
77
+ const buckets = {};
78
+ for (const key of BUCKET_KEYS) {
79
+ const count = record[key];
80
+ if (!isCount(count))
81
+ return null;
82
+ buckets[key] = count;
83
+ }
84
+ return buckets;
85
+ }
67
86
  function bucketsOf(usage) {
68
87
  return {
69
88
  input: usage.uncachedInputTokens,
@@ -168,6 +187,55 @@ class ClaudeUsageDeduper {
168
187
  reset() {
169
188
  this.seen.clear();
170
189
  }
190
+ /**
191
+ * The most recent `limit` message ids and the tokens counted for each,
192
+ * oldest first. Split lines of one response are adjacent, so a reader
193
+ * resumed at the position this was taken at needs only the last few.
194
+ */
195
+ exportState(limit = DEFAULT_EXPORT_LIMIT) {
196
+ const count = limit >= 0 ? Math.min(this.seen.size, Math.floor(limit)) : 0;
197
+ const entries = [];
198
+ if (count > 0) {
199
+ let skip = this.seen.size - count;
200
+ for (const [id, buckets] of this.seen) {
201
+ if (skip > 0) {
202
+ skip -= 1;
203
+ continue;
204
+ }
205
+ entries.push([id, { ...buckets }]);
206
+ }
207
+ }
208
+ return { version: 1, entries };
209
+ }
210
+ /**
211
+ * Replaces this deduper's state with one from {@link exportState}. Returns
212
+ * false, changing nothing, when `state` is not a valid version-1 state
213
+ * (wrong shape, empty ids, or counts that are not finite non-negative
214
+ * numbers). Entries beyond this deduper's capacity drop oldest first.
215
+ */
216
+ importState(state) {
217
+ if (!state || typeof state !== 'object')
218
+ return false;
219
+ const { version, entries } = state;
220
+ if (version !== 1 || !Array.isArray(entries))
221
+ return false;
222
+ const parsed = [];
223
+ for (const entry of entries) {
224
+ if (!Array.isArray(entry) || entry.length !== 2)
225
+ return false;
226
+ const [id, value] = entry;
227
+ if (typeof id !== 'string' || id.length === 0)
228
+ return false;
229
+ const buckets = bucketsFrom(value);
230
+ if (!buckets)
231
+ return false;
232
+ parsed.push([id, buckets]);
233
+ }
234
+ this.seen.clear();
235
+ for (const [id, buckets] of parsed)
236
+ this.remember(id, buckets);
237
+ return true;
238
+ }
171
239
  remember(id, buckets) {
172
240
  this.seen.delete(id);
173
241
  this.seen.set(id, buckets);
@@ -11,9 +11,19 @@
11
11
  * every hit and pruned least-recently-used, once per collection.
12
12
  *
13
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.)
14
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session,
15
+ * tagged with the subagent, and their sizes and mtimes join the fingerprint.
16
+ * (Codex spawned subagents and forks are top-level rollouts, already listed
17
+ * as sessions of their own; their events are tagged with the parent read
18
+ * from the rollout's first line.)
19
+ *
20
+ * Each event also carries the ordinal of the human prompt it answered, from
21
+ * the same scanner as `collectPromptHistory()`, so usage joins prompt history
22
+ * on `(provider, sessionId, ordinal)`.
23
+ *
24
+ * Cache files are named per cache version (`<provider>--<session>.v3.json`)
25
+ * so readers of different versions sharing the directory never overwrite each
26
+ * other's files; pruning ages out every version's files alike.
17
27
  *
18
28
  * Node-only: reads session files and the cache directory.
19
29
  *
@@ -25,9 +35,11 @@ import type { PricingProvenance } from '../usageNormalization';
25
35
  /**
26
36
  * Bump when the cached record shape or the counting rules change; older files
27
37
  * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
28
- * are counted once, and Claude subagent transcripts are included.
38
+ * are counted once, and Claude subagent transcripts are included. v3: events
39
+ * keep reasoning tokens, prompt ordinal, message id, and subagent, and files
40
+ * move to a versioned name (`<provider>--<session>.v3.json`).
29
41
  */
30
- export declare const USAGE_CACHE_VERSION = 2;
42
+ export declare const USAGE_CACHE_VERSION = 3;
31
43
  /**
32
44
  * Most-recently-used cache files kept on disk. Sized for a busy month of
33
45
  * sessions (files are a few KB each); a collection larger than this still
@@ -47,11 +59,43 @@ export interface UsageEventRecord {
47
59
  /** Four buckets plus the provider-semantics-aware total. */
48
60
  tokens: TokenTotals & {
49
61
  totalTokens: number;
62
+ /**
63
+ * Reasoning tokens the provider reported, when any. Informational, never
64
+ * added to a total: Codex counts them inside `outputTokens`; OpenCode
65
+ * reports them apart, inside `totalTokens` but not `outputTokens`.
66
+ */
67
+ reasoningTokens?: number;
50
68
  };
51
69
  costUsd: number | null;
52
70
  costProvenance: PricingProvenance;
53
71
  /** Session file the event came from; tells apart two files with one session id. */
54
72
  filePath?: string;
73
+ /**
74
+ * Ordinal of the human prompt this call followed: the highest ordinal among
75
+ * the session's prompts timestamped at or before the event, as
76
+ * `collectPromptHistory()` numbers them with default bounds. Claude Code
77
+ * subagent calls map to the parent session's prompts. `null` when no prompt
78
+ * precedes the call, or the session has none (a Codex subagent or fork is a
79
+ * session of its own without human prompts). Absent when
80
+ * unknown: a provider without prompt history (OpenCode), or a cache entry
81
+ * written before prompt ordinals were recorded.
82
+ */
83
+ promptOrdinal?: number | null;
84
+ /**
85
+ * The provider's message id for the call (Claude Code, OpenCode). Absent for
86
+ * Codex, whose `token_count` lines carry no request id (the reader's
87
+ * timestamp-derived placeholder is not passed through).
88
+ */
89
+ requestId?: string;
90
+ /**
91
+ * Set when a subagent made the call. Claude Code: the agent id from its
92
+ * transcript name (`agent-<id>.jsonl`), parent = this session. Codex: the
93
+ * forked or spawned rollout's own session id and the session it came from.
94
+ */
95
+ subagent?: {
96
+ agentId: string;
97
+ parentSessionId: string;
98
+ };
55
99
  }
56
100
  /** One session that contributed events. */
57
101
  export interface UsageSessionRecord {
@@ -111,7 +155,11 @@ export interface CollectUsageEventsResult {
111
155
  roots?: string[];
112
156
  }
113
157
  export declare function getUsageCacheDir(): string;
114
- /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
158
+ /**
159
+ * Deletes all but the `keep` most recently used cache files (by mtime), of
160
+ * every cache version alike, so files no reader touches any more age out.
161
+ * Best-effort.
162
+ */
115
163
  export declare function pruneUsageCache(keep?: number, dir?: string): number;
116
164
  /**
117
165
  * Collect priced usage events for every session touched in the window.
@@ -12,9 +12,19 @@
12
12
  * every hit and pruned least-recently-used, once per collection.
13
13
  *
14
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.)
15
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session,
16
+ * tagged with the subagent, and their sizes and mtimes join the fingerprint.
17
+ * (Codex spawned subagents and forks are top-level rollouts, already listed
18
+ * as sessions of their own; their events are tagged with the parent read
19
+ * from the rollout's first line.)
20
+ *
21
+ * Each event also carries the ordinal of the human prompt it answered, from
22
+ * the same scanner as `collectPromptHistory()`, so usage joins prompt history
23
+ * on `(provider, sessionId, ordinal)`.
24
+ *
25
+ * Cache files are named per cache version (`<provider>--<session>.v3.json`)
26
+ * so readers of different versions sharing the directory never overwrite each
27
+ * other's files; pruning ages out every version's files alike.
18
28
  *
19
29
  * Node-only: reads session files and the cache directory.
20
30
  *
@@ -60,7 +70,10 @@ exports.pruneUsageCache = pruneUsageCache;
60
70
  exports.collectUsageEvents = collectUsageEvents;
61
71
  const fs = __importStar(require("fs"));
62
72
  const path = __importStar(require("path"));
73
+ const codexRolloutOrigin_1 = require("../parsers/codexRolloutOrigin");
63
74
  const paths_1 = require("../paths");
75
+ const promptHistory_1 = require("../promptHistory");
76
+ const codexRollouts_1 = require("../providers/codexRollouts");
64
77
  const sessionPreviews_1 = require("../sessionPreviews");
65
78
  const usageNormalization_1 = require("../usageNormalization");
66
79
  const atomic_1 = require("../writers/atomic");
@@ -68,9 +81,11 @@ const workspaceRoots_1 = require("../workspaceRoots");
68
81
  /**
69
82
  * Bump when the cached record shape or the counting rules change; older files
70
83
  * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
71
- * are counted once, and Claude subagent transcripts are included.
84
+ * are counted once, and Claude subagent transcripts are included. v3: events
85
+ * keep reasoning tokens, prompt ordinal, message id, and subagent, and files
86
+ * move to a versioned name (`<provider>--<session>.v3.json`).
72
87
  */
73
- exports.USAGE_CACHE_VERSION = 2;
88
+ exports.USAGE_CACHE_VERSION = 3;
74
89
  /**
75
90
  * Most-recently-used cache files kept on disk. Sized for a busy month of
76
91
  * sessions (files are a few KB each); a collection larger than this still
@@ -81,9 +96,15 @@ exports.MAX_USAGE_CACHE_FILES = 5000;
81
96
  function getUsageCacheDir() {
82
97
  return path.join((0, paths_1.getConfigDir)(), 'usage-cache');
83
98
  }
99
+ /**
100
+ * Cache file of one session. The name carries the cache version, so a reader
101
+ * of another version sharing the directory (which rejects this version, and
102
+ * versions before 3 use the unversioned `<provider>--<session>.json`) never
103
+ * overwrites it, and this version never overwrites theirs.
104
+ */
84
105
  function cacheFilePath(dir, provider, sessionId) {
85
106
  const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_');
86
- return path.join(dir, `${provider}--${safe}.json`);
107
+ return path.join(dir, `${provider}--${safe}.v${exports.USAGE_CACHE_VERSION}.json`);
87
108
  }
88
109
  function toMs(value, label) {
89
110
  if (value === undefined)
@@ -114,7 +135,11 @@ function readCache(filePath) {
114
135
  return null;
115
136
  }
116
137
  }
117
- /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
138
+ /**
139
+ * Deletes all but the `keep` most recently used cache files (by mtime), of
140
+ * every cache version alike, so files no reader touches any more age out.
141
+ * Best-effort.
142
+ */
118
143
  function pruneUsageCache(keep = exports.MAX_USAGE_CACHE_FILES, dir = getUsageCacheDir()) {
119
144
  try {
120
145
  const entries = fs
@@ -147,7 +172,7 @@ function pruneUsageCache(keep = exports.MAX_USAGE_CACHE_FILES, dir = getUsageCac
147
172
  return 0;
148
173
  }
149
174
  }
150
- function cachedEventFromUsage(usage, timestamp, model) {
175
+ function cachedEventFromUsage(usage, timestamp, model, messageId, agentId) {
151
176
  const record = {
152
177
  t: timestamp,
153
178
  m: model,
@@ -160,16 +185,29 @@ function cachedEventFromUsage(usage, timestamp, model) {
160
185
  };
161
186
  if (usage.reportedCostUsd !== undefined)
162
187
  record.rc = usage.reportedCostUsd;
188
+ if (usage.reasoningTokens > 0)
189
+ record.rt = usage.reasoningTokens;
190
+ if (messageId && !messageId.startsWith('token-count-'))
191
+ record.rid = messageId;
192
+ if (agentId)
193
+ record.sa = agentId;
163
194
  return record;
164
195
  }
165
- /** Price a cached event with the current catalog (or its provider-reported cost). */
166
- function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
196
+ /**
197
+ * Price a cached event with the current catalog (or its provider-reported
198
+ * cost). Output is priced from the billable output (`bo`), which already
199
+ * holds reasoning however the provider reports it, so restoring the reasoning
200
+ * count never changes the cost.
201
+ */
202
+ function priceCachedEvent(cached, provider, sessionId, project, sessionFile, sessionSubagent) {
203
+ const reasoningTokens = typeof cached.rt === 'number' && cached.rt > 0 ? cached.rt : 0;
167
204
  const usage = {
168
205
  uncachedInputTokens: cached.i,
169
206
  cacheReadTokens: cached.cr,
170
207
  cacheWriteTokens: cached.cw,
171
208
  outputTokens: cached.o,
172
- reasoningTokens: 0,
209
+ reasoningTokens,
210
+ // `bo` is already the billable output; nothing is added to it.
173
211
  reasoningIncludedInOutput: true,
174
212
  cacheInclusiveInputTokens: cached.i + cached.cr + cached.cw,
175
213
  billableOutputTokens: cached.bo,
@@ -181,6 +219,9 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
181
219
  usage,
182
220
  modelId: cached.m === 'unknown' ? undefined : cached.m,
183
221
  });
222
+ const subagent = typeof cached.sa === 'string' && cached.sa
223
+ ? { agentId: cached.sa, parentSessionId: sessionId }
224
+ : sessionSubagent;
184
225
  return {
185
226
  timestamp: cached.t,
186
227
  provider,
@@ -193,13 +234,19 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
193
234
  cacheWriteTokens: cached.cw,
194
235
  cacheReadTokens: cached.cr,
195
236
  totalTokens: cached.tt,
237
+ ...(reasoningTokens > 0 ? { reasoningTokens } : {}),
196
238
  },
197
239
  costUsd: priced.costUsd,
198
240
  costProvenance: priced.source,
199
241
  filePath: sessionFile,
242
+ ...(cached.po === null || (typeof cached.po === 'number' && Number.isInteger(cached.po))
243
+ ? { promptOrdinal: cached.po }
244
+ : {}),
245
+ ...(typeof cached.rid === 'string' ? { requestId: cached.rid } : {}),
246
+ ...(subagent ? { subagent: { ...subagent } } : {}),
200
247
  };
201
248
  }
202
- const CLAUDE_AGENT_FILE = /^agent-.+\.jsonl$/;
249
+ const CLAUDE_AGENT_FILE = /^agent-(.+)\.jsonl$/;
203
250
  /** Claude Code subagent transcripts of a session, sorted by name. */
204
251
  function claudeSubagentFiles(preview) {
205
252
  if (preview.provider !== 'claude-code')
@@ -219,6 +266,7 @@ function claudeSubagentFiles(preview) {
219
266
  files.push({
220
267
  path: path.join(dir, name),
221
268
  name,
269
+ agentId: CLAUDE_AGENT_FILE.exec(name)?.[1] ?? name,
222
270
  sizeBytes: stat.size,
223
271
  modifiedAt: stat.mtimeMs,
224
272
  });
@@ -235,7 +283,7 @@ function sessionFingerprint(preview, subagents) {
235
283
  return base;
236
284
  return `${base}|${subagents.map((f) => `${f.name}:${f.sizeBytes}:${f.modifiedAt}`).join(',')}`;
237
285
  }
238
- function readTranscriptUsage(provider, filePath, cached) {
286
+ function readTranscriptUsage(provider, filePath, cached, agentId) {
239
287
  const reader = provider.createReader(filePath);
240
288
  const events = reader.readAll();
241
289
  reader.flush();
@@ -257,12 +305,14 @@ function readTranscriptUsage(provider, filePath, cached) {
257
305
  target.cr += usage.cacheReadTokens;
258
306
  target.bo += usage.billableOutputTokens;
259
307
  target.tt += usage.totalTokens;
308
+ if (usage.reasoningTokens > 0)
309
+ target.rt = (target.rt ?? 0) + usage.reasoningTokens;
260
310
  continue;
261
311
  }
262
312
  const timestamp = Date.parse(event.timestamp);
263
313
  if (!Number.isFinite(timestamp))
264
314
  continue;
265
- const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown');
315
+ const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown', messageId, agentId);
266
316
  cached.push(record);
267
317
  if (messageId)
268
318
  byMessageId.set(messageId, record);
@@ -272,10 +322,75 @@ function readSessionUsage(provider, preview, subagents) {
272
322
  const cached = [];
273
323
  readTranscriptUsage(provider, preview.filePath, cached);
274
324
  for (const file of subagents)
275
- readTranscriptUsage(provider, file.path, cached);
325
+ readTranscriptUsage(provider, file.path, cached, file.agentId);
276
326
  cached.sort((a, b) => a.t - b.t);
277
327
  return cached;
278
328
  }
329
+ /**
330
+ * Sets each event's `po` to the highest ordinal among the prompts timestamped
331
+ * at or before it, or null when none is. With prompts in time order (the
332
+ * usual case) that is the last prompt before the event.
333
+ */
334
+ function assignPromptOrdinals(events, marks) {
335
+ const sorted = [...marks].sort((a, b) => a.timestampMs - b.timestampMs || a.ordinal - b.ordinal);
336
+ const highest = [];
337
+ let max = -1;
338
+ for (const mark of sorted) {
339
+ max = Math.max(max, mark.ordinal);
340
+ highest.push(max);
341
+ }
342
+ for (const event of events) {
343
+ let low = 0;
344
+ let high = sorted.length;
345
+ while (low < high) {
346
+ const middle = (low + high) >>> 1;
347
+ if (sorted[middle].timestampMs <= event.t)
348
+ low = middle + 1;
349
+ else
350
+ high = middle;
351
+ }
352
+ event.po = low === 0 ? null : highest[low - 1];
353
+ }
354
+ }
355
+ /**
356
+ * Prompt ordinals for a freshly read session, from its main transcript.
357
+ * Returns false when the transcript could not be scanned (the events then
358
+ * keep no ordinal and the session is not cached, so the next run retries).
359
+ */
360
+ async function addPromptOrdinals(preview, events) {
361
+ if (preview.provider !== 'claude-code' && preview.provider !== 'codex')
362
+ return true;
363
+ let marks;
364
+ try {
365
+ marks = await (0, promptHistory_1.readPromptOrdinalMarks)(preview.provider, preview.sessionId, preview.filePath);
366
+ }
367
+ catch {
368
+ return false;
369
+ }
370
+ assignPromptOrdinals(events, marks);
371
+ return true;
372
+ }
373
+ /** Parent of a Codex rollout that is a fork or a spawned subagent, from its first line only. */
374
+ async function codexSessionSubagent(preview) {
375
+ if (preview.provider !== 'codex')
376
+ return undefined;
377
+ const { line } = await (0, codexRollouts_1.readFirstLineAsync)(preview.filePath);
378
+ if (!line)
379
+ return undefined;
380
+ let first;
381
+ try {
382
+ first = JSON.parse(line);
383
+ }
384
+ catch {
385
+ return undefined;
386
+ }
387
+ const origin = (0, codexRolloutOrigin_1.codexRolloutOrigin)(first);
388
+ if (!origin.parentId || (!origin.isSubagent && !origin.forkedFromId))
389
+ return undefined;
390
+ if (origin.parentId === preview.sessionId)
391
+ return undefined;
392
+ return { agentId: preview.sessionId, parentSessionId: origin.parentId };
393
+ }
279
394
  function yieldToEventLoop() {
280
395
  return new Promise((resolve) => setImmediate(resolve));
281
396
  }
@@ -315,12 +430,14 @@ async function collectUsageEvents(options) {
315
430
  const fingerprint = sessionFingerprint(preview, subagentFiles);
316
431
  const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
317
432
  let cachedEvents = null;
433
+ let sessionSubagent;
318
434
  let project = preview.workspacePath;
319
435
  let fromCache = false;
320
436
  if (useCache) {
321
437
  const cached = readCache(filePath);
322
438
  if (cached && cached.fingerprint === fingerprint && cached.sessionId === preview.sessionId) {
323
439
  cachedEvents = cached.events;
440
+ sessionSubagent = validSubagent(cached.subagent);
324
441
  project = project ?? cached.project;
325
442
  fromCache = true;
326
443
  cacheHits += 1;
@@ -344,7 +461,10 @@ async function collectUsageEvents(options) {
344
461
  options.onDiagnostic?.(diagnostic);
345
462
  continue;
346
463
  }
347
- if (useCache) {
464
+ // Ordinals and origin are best-effort: a failure costs only the cache write.
465
+ const ordinalsRead = await addPromptOrdinals(preview, cachedEvents);
466
+ sessionSubagent = await codexSessionSubagent(preview).catch(() => undefined);
467
+ if (useCache && ordinalsRead) {
348
468
  const record = {
349
469
  version: exports.USAGE_CACHE_VERSION,
350
470
  provider: preview.provider,
@@ -352,6 +472,7 @@ async function collectUsageEvents(options) {
352
472
  fingerprint,
353
473
  project,
354
474
  events: cachedEvents,
475
+ ...(sessionSubagent ? { subagent: sessionSubagent } : {}),
355
476
  };
356
477
  try {
357
478
  await (0, atomic_1.atomicWriteJson)(filePath, record);
@@ -371,7 +492,7 @@ async function collectUsageEvents(options) {
371
492
  continue;
372
493
  if (untilMs !== null && cached.t > untilMs)
373
494
  continue;
374
- events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath));
495
+ events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath, sessionSubagent));
375
496
  first = first === null ? cached.t : Math.min(first, cached.t);
376
497
  last = last === null ? cached.t : Math.max(last, cached.t);
377
498
  count += 1;
@@ -400,6 +521,14 @@ async function collectUsageEvents(options) {
400
521
  ...(scope ? { roots: scope.roots } : {}),
401
522
  };
402
523
  }
524
+ function validSubagent(value) {
525
+ if (typeof value !== 'object' || value === null)
526
+ return undefined;
527
+ const { agentId, parentSessionId } = value;
528
+ return typeof agentId === 'string' && typeof parentSessionId === 'string'
529
+ ? { agentId, parentSessionId }
530
+ : undefined;
531
+ }
403
532
  /** The roots to list, or undefined for an unscoped (or legacy single-root) call. */
404
533
  function usageScope(options) {
405
534
  if (options.workspacePaths) {
@@ -10,8 +10,11 @@ import type { ProviderId } from '../providers/types';
10
10
  import type { TokenTotals } from '../types/historicalData';
11
11
  import type { UsageEventRecord } from './usageEvents';
12
12
  export type UsageGranularity = 'day' | 'week' | 'month' | 'session';
13
- /** Extra dimensions a bucket is split by, beyond the time key. */
14
- export type UsageGroupDimension = 'provider' | 'project' | 'model';
13
+ /**
14
+ * Extra dimensions a bucket is split by, beyond the time key. `session`
15
+ * splits each time bucket per session (rows then carry `sessionId`).
16
+ */
17
+ export type UsageGroupDimension = 'provider' | 'project' | 'model' | 'session';
15
18
  export interface BucketUsageOptions {
16
19
  granularity: UsageGranularity;
17
20
  /** Key by the UTC calendar instead of the local calendar (default false). */
@@ -20,6 +23,19 @@ export interface BucketUsageOptions {
20
23
  weekStartsOn?: 0 | 1;
21
24
  /** Dimensions to group by in addition to the time key (default `['provider']`). */
22
25
  groupBy?: readonly UsageGroupDimension[];
26
+ /**
27
+ * Drop events whose model is Claude Code's `<synthetic>` placeholder
28
+ * (`SYNTHETIC_MODEL`: locally generated, unbilled messages). Default false.
29
+ */
30
+ excludeSynthetic?: boolean;
31
+ /**
32
+ * How model ids are keyed in `model` rows and the `models` list. `'raw'`
33
+ * (default) keeps each id as recorded; `'canonical'` merges spellings of one
34
+ * model (date suffixes, provider/region prefixes, Bedrock forms) through
35
+ * `canonicalModelId()`, so `claude-opus-5-5-20261001` and `claude-opus-5-5`
36
+ * share a row. Pricing is unaffected: events arrive already priced.
37
+ */
38
+ modelKey?: 'raw' | 'canonical';
23
39
  }
24
40
  export interface UsageBucketRow {
25
41
  /** `YYYY-MM-DD` for days and week starts, `YYYY-MM` for months, the session id for sessions. */
@@ -29,7 +45,7 @@ export interface UsageBucketRow {
29
45
  project: string | null;
30
46
  /** Set when grouping by model; null for rows spanning every model. */
31
47
  model: string | null;
32
- /** Session id for `session` rows; null otherwise. */
48
+ /** Session id for `session` rows and rows grouped by `session`; null otherwise. */
33
49
  sessionId: string | null;
34
50
  /** Models seen in the row, most tokens first. */
35
51
  models: string[];
@@ -41,8 +57,18 @@ export interface UsageBucketRow {
41
57
  totalTokens: number;
42
58
  /** Priced portion of the cost; read `costProvenance` before labelling it. */
43
59
  costUsd: number;
60
+ /**
61
+ * Sum of the priced calls' costs, or null when no call in the row was
62
+ * priced (where `costUsd` reads 0).
63
+ */
64
+ pricedCostUsd: number | null;
44
65
  costProvenance: AggregatedCostProvenance;
45
66
  unpricedCalls: number;
67
+ /**
68
+ * Reasoning tokens reported by the row's calls. Informational, already
69
+ * inside the token buckets and `totalTokens`. Always set by `bucketUsage`.
70
+ */
71
+ reasoningTokens?: number;
46
72
  /** First and last event in the row, ms since epoch. */
47
73
  firstTimestamp: number;
48
74
  lastTimestamp: number;
@@ -53,8 +79,12 @@ export interface UsageTotals {
53
79
  tokens: TokenTotals;
54
80
  totalTokens: number;
55
81
  costUsd: number;
82
+ /** Sum of the rows' priced costs, or null when no row had a priced call. */
83
+ pricedCostUsd: number | null;
56
84
  costProvenance: AggregatedCostProvenance;
57
85
  unpricedCalls: number;
86
+ /** Sum of the rows' `reasoningTokens`. Always set by `summarizeUsageRows`. */
87
+ reasoningTokens?: number;
58
88
  }
59
89
  /** The `YYYY-MM-DD` key of the week containing `ms`, starting on `weekStartsOn`. */
60
90
  export declare function weekKey(ms: number, options?: {
@@ -65,7 +95,8 @@ export declare function weekKey(ms: number, options?: {
65
95
  export declare function usageBucketKey(event: Pick<UsageEventRecord, 'timestamp' | 'sessionId'>, options: BucketUsageOptions): string;
66
96
  /**
67
97
  * Group usage events into rows. Rows are sorted by key (sessions by first
68
- * event), then provider, project, and model, so tables read oldest first.
98
+ * event), then provider, project, and model, so tables read oldest first;
99
+ * rows grouped by `session` within one of those follow their first event.
69
100
  */
70
101
  export declare function bucketUsage(events: readonly UsageEventRecord[], options: BucketUsageOptions): UsageBucketRow[];
71
102
  /**