sidekick-shared 0.27.2 → 0.27.4
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.
- package/CHANGELOG.md +34 -0
- package/README.md +57 -14
- package/dist/aggregation/EventAggregator.d.ts +12 -2
- package/dist/aggregation/EventAggregator.js +49 -19
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +9 -2
- package/dist/extractors/tokenUsage.js +1 -0
- package/dist/formatters/sessionDump.d.ts +10 -2
- package/dist/formatters/sessionDump.js +45 -23
- package/dist/index.d.ts +7 -3
- package/dist/index.js +31 -10
- package/dist/node.d.ts +6 -1
- package/dist/node.js +12 -2
- package/dist/parsers/codexParser.d.ts +22 -2
- package/dist/parsers/codexParser.js +64 -5
- package/dist/parsers/subagentScanner.d.ts +9 -0
- package/dist/parsers/subagentScanner.js +45 -12
- package/dist/parsers/subagentStatsCache.d.ts +17 -0
- package/dist/parsers/subagentStatsCache.js +82 -0
- package/dist/parsers/subagentTraceParser.js +19 -10
- package/dist/providers/claudeCode.js +8 -41
- package/dist/providers/codex.js +28 -14
- package/dist/providers/codexDatabase.d.ts +16 -1
- package/dist/providers/codexDatabase.js +49 -1
- package/dist/providers/openCode.js +30 -4
- package/dist/report/htmlReportGenerator.js +33 -11
- package/dist/report/sessionReportInputs.d.ts +3 -1
- package/dist/report/sessionReportInputs.js +2 -0
- package/dist/report/types.d.ts +5 -0
- package/dist/schemas/sessionEvent.d.ts +30 -6
- package/dist/schemas/sessionEvent.js +7 -1
- package/dist/schemas/stateFile.d.ts +2 -0
- package/dist/schemas/stateFile.js +1 -0
- package/dist/sessionTokenTotals.d.ts +46 -0
- package/dist/sessionTokenTotals.js +138 -0
- package/dist/stateFile.d.ts +7 -0
- package/dist/tokenSummary.d.ts +27 -1
- package/dist/tokenSummary.js +50 -3
- package/dist/types/codex.d.ts +4 -0
- package/dist/types/sessionEvent.d.ts +22 -0
- package/dist/usage/claudeUsageDedupe.d.ts +53 -0
- package/dist/usage/claudeUsageDedupe.js +195 -0
- package/dist/usage/subagentUsage.d.ts +13 -0
- package/dist/usage/subagentUsage.js +21 -0
- package/dist/usage/usageEvents.d.ts +18 -2
- package/dist/usage/usageEvents.js +83 -8
- package/dist/watchers/eventBridge.js +5 -0
- package/dist/watchers/jsonlWatcher.d.ts +5 -0
- package/dist/watchers/jsonlWatcher.js +40 -5
- package/dist/watchers/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code writes one API response as several JSONL lines — one per content
|
|
3
|
+
* block — and every line repeats that response's `message.usage`. Streaming
|
|
4
|
+
* also means early lines can carry a partial `output_tokens` (often 1) that a
|
|
5
|
+
* later line of the same `message.id` supersedes. Summing every line
|
|
6
|
+
* therefore roughly doubles a session's tokens and cost.
|
|
7
|
+
*
|
|
8
|
+
* The correct rule is "the last copy of a message's usage wins", but readers
|
|
9
|
+
* stream events and cannot retract what they already emitted. So the first
|
|
10
|
+
* line carries the usage as a normal call, identical repeats are stripped, and
|
|
11
|
+
* a repeat that grew emits only the growth as a `usageKind: 'correction'`
|
|
12
|
+
* top-up. Consumers add corrections to token and cost totals without counting
|
|
13
|
+
* another call or context sample.
|
|
14
|
+
*
|
|
15
|
+
* Browser-safe: no Node imports.
|
|
16
|
+
*
|
|
17
|
+
* @module usage/claudeUsageDedupe
|
|
18
|
+
*/
|
|
19
|
+
import { type NormalizedUsage } from '../usageNormalization';
|
|
20
|
+
import type { SessionEvent } from '../types/sessionEvent';
|
|
21
|
+
/** Claude Code's placeholder model for locally generated (unbilled) messages. */
|
|
22
|
+
export declare const SYNTHETIC_MODEL = "<synthetic>";
|
|
23
|
+
/** Attach Anthropic-semantics normalized usage and Claude Code provenance to an event. */
|
|
24
|
+
export declare function normalizeClaudeUsage(event: SessionEvent): SessionEvent;
|
|
25
|
+
/**
|
|
26
|
+
* Per-reader dedupe state. Split lines of one response are adjacent, so a
|
|
27
|
+
* bounded map (oldest keys evicted first) is enough.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ClaudeUsageDeduper {
|
|
30
|
+
private readonly maxKeys;
|
|
31
|
+
private readonly seen;
|
|
32
|
+
constructor(maxKeys?: number);
|
|
33
|
+
/**
|
|
34
|
+
* Returns the event with its usage adjusted: unchanged for the first line of
|
|
35
|
+
* a message, stripped for an identical repeat or a `<synthetic>` message,
|
|
36
|
+
* and replaced by the growth (marked as a correction) for a repeat that grew.
|
|
37
|
+
* Events must already carry `message.normalizedUsage`.
|
|
38
|
+
*/
|
|
39
|
+
apply(event: SessionEvent): SessionEvent;
|
|
40
|
+
/** Whether a message id has already been counted by this deduper. */
|
|
41
|
+
has(id: string): boolean;
|
|
42
|
+
reset(): void;
|
|
43
|
+
private remember;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deduplicated normalized usage for one raw Claude Code JSONL assistant line,
|
|
47
|
+
* or null when the line carries none (or only repeats usage already counted).
|
|
48
|
+
*/
|
|
49
|
+
export declare function dedupedRawClaudeUsage(raw: {
|
|
50
|
+
type?: string;
|
|
51
|
+
timestamp?: string;
|
|
52
|
+
message?: SessionEvent['message'];
|
|
53
|
+
} | null | undefined, deduper: ClaudeUsageDeduper): NormalizedUsage | null;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code writes one API response as several JSONL lines — one per content
|
|
4
|
+
* block — and every line repeats that response's `message.usage`. Streaming
|
|
5
|
+
* also means early lines can carry a partial `output_tokens` (often 1) that a
|
|
6
|
+
* later line of the same `message.id` supersedes. Summing every line
|
|
7
|
+
* therefore roughly doubles a session's tokens and cost.
|
|
8
|
+
*
|
|
9
|
+
* The correct rule is "the last copy of a message's usage wins", but readers
|
|
10
|
+
* stream events and cannot retract what they already emitted. So the first
|
|
11
|
+
* line carries the usage as a normal call, identical repeats are stripped, and
|
|
12
|
+
* a repeat that grew emits only the growth as a `usageKind: 'correction'`
|
|
13
|
+
* top-up. Consumers add corrections to token and cost totals without counting
|
|
14
|
+
* another call or context sample.
|
|
15
|
+
*
|
|
16
|
+
* Browser-safe: no Node imports.
|
|
17
|
+
*
|
|
18
|
+
* @module usage/claudeUsageDedupe
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.ClaudeUsageDeduper = exports.SYNTHETIC_MODEL = void 0;
|
|
22
|
+
exports.normalizeClaudeUsage = normalizeClaudeUsage;
|
|
23
|
+
exports.dedupedRawClaudeUsage = dedupedRawClaudeUsage;
|
|
24
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
25
|
+
/** Claude Code's placeholder model for locally generated (unbilled) messages. */
|
|
26
|
+
exports.SYNTHETIC_MODEL = '<synthetic>';
|
|
27
|
+
const DEFAULT_MAX_KEYS = 4096;
|
|
28
|
+
/** Attach Anthropic-semantics normalized usage and Claude Code provenance to an event. */
|
|
29
|
+
function normalizeClaudeUsage(event) {
|
|
30
|
+
const message = event.message;
|
|
31
|
+
const usage = message?.usage;
|
|
32
|
+
if (!message || !usage) {
|
|
33
|
+
return {
|
|
34
|
+
...event,
|
|
35
|
+
providerMetadata: {
|
|
36
|
+
...event.providerMetadata,
|
|
37
|
+
providerId: 'claude-code',
|
|
38
|
+
source: 'claude-code-jsonl',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...event,
|
|
44
|
+
message: {
|
|
45
|
+
...message,
|
|
46
|
+
normalizedUsage: (0, usageNormalization_1.normalizeProviderUsage)({
|
|
47
|
+
semantics: 'anthropic',
|
|
48
|
+
provider: 'anthropic',
|
|
49
|
+
source: 'claude-code-jsonl',
|
|
50
|
+
model: message.model,
|
|
51
|
+
inputTokens: usage.input_tokens,
|
|
52
|
+
outputTokens: usage.output_tokens,
|
|
53
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
54
|
+
cacheWriteTokens: usage.cache_creation_input_tokens,
|
|
55
|
+
reasoningTokens: usage.reasoning_tokens,
|
|
56
|
+
reasoningIncludedInOutput: false,
|
|
57
|
+
reportedCostUsd: usage.reported_cost,
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
providerMetadata: {
|
|
61
|
+
...event.providerMetadata,
|
|
62
|
+
providerId: 'claude-code',
|
|
63
|
+
source: 'claude-code-jsonl',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function bucketsOf(usage) {
|
|
68
|
+
return {
|
|
69
|
+
input: usage.uncachedInputTokens,
|
|
70
|
+
cacheRead: usage.cacheReadTokens,
|
|
71
|
+
cacheWrite: usage.cacheWriteTokens,
|
|
72
|
+
output: usage.outputTokens,
|
|
73
|
+
reasoning: usage.reasoningTokens,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function stripUsage(event) {
|
|
77
|
+
if (!event.message)
|
|
78
|
+
return event;
|
|
79
|
+
const message = { ...event.message };
|
|
80
|
+
delete message.usage;
|
|
81
|
+
delete message.normalizedUsage;
|
|
82
|
+
return { ...event, message };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Per-reader dedupe state. Split lines of one response are adjacent, so a
|
|
86
|
+
* bounded map (oldest keys evicted first) is enough.
|
|
87
|
+
*/
|
|
88
|
+
class ClaudeUsageDeduper {
|
|
89
|
+
maxKeys;
|
|
90
|
+
seen = new Map();
|
|
91
|
+
constructor(maxKeys = DEFAULT_MAX_KEYS) {
|
|
92
|
+
this.maxKeys = maxKeys;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns the event with its usage adjusted: unchanged for the first line of
|
|
96
|
+
* a message, stripped for an identical repeat or a `<synthetic>` message,
|
|
97
|
+
* and replaced by the growth (marked as a correction) for a repeat that grew.
|
|
98
|
+
* Events must already carry `message.normalizedUsage`.
|
|
99
|
+
*/
|
|
100
|
+
apply(event) {
|
|
101
|
+
const message = event.message;
|
|
102
|
+
const usage = message?.normalizedUsage;
|
|
103
|
+
if (!message || !usage)
|
|
104
|
+
return event;
|
|
105
|
+
if (message.model === exports.SYNTHETIC_MODEL)
|
|
106
|
+
return stripUsage(event);
|
|
107
|
+
const id = message.id;
|
|
108
|
+
if (!id)
|
|
109
|
+
return event;
|
|
110
|
+
const current = bucketsOf(usage);
|
|
111
|
+
const previous = this.seen.get(id);
|
|
112
|
+
if (!previous) {
|
|
113
|
+
this.remember(id, current);
|
|
114
|
+
return event;
|
|
115
|
+
}
|
|
116
|
+
const delta = {
|
|
117
|
+
input: Math.max(0, current.input - previous.input),
|
|
118
|
+
cacheRead: Math.max(0, current.cacheRead - previous.cacheRead),
|
|
119
|
+
cacheWrite: Math.max(0, current.cacheWrite - previous.cacheWrite),
|
|
120
|
+
output: Math.max(0, current.output - previous.output),
|
|
121
|
+
reasoning: Math.max(0, current.reasoning - previous.reasoning),
|
|
122
|
+
};
|
|
123
|
+
this.remember(id, {
|
|
124
|
+
input: previous.input + delta.input,
|
|
125
|
+
cacheRead: previous.cacheRead + delta.cacheRead,
|
|
126
|
+
cacheWrite: previous.cacheWrite + delta.cacheWrite,
|
|
127
|
+
output: previous.output + delta.output,
|
|
128
|
+
reasoning: previous.reasoning + delta.reasoning,
|
|
129
|
+
});
|
|
130
|
+
if (!delta.input &&
|
|
131
|
+
!delta.cacheRead &&
|
|
132
|
+
!delta.cacheWrite &&
|
|
133
|
+
!delta.output &&
|
|
134
|
+
!delta.reasoning) {
|
|
135
|
+
return stripUsage(event);
|
|
136
|
+
}
|
|
137
|
+
const cacheInclusiveInputTokens = delta.input + delta.cacheRead + delta.cacheWrite;
|
|
138
|
+
const billableOutputTokens = delta.output + (usage.reasoningIncludedInOutput ? 0 : delta.reasoning);
|
|
139
|
+
const normalizedUsage = {
|
|
140
|
+
uncachedInputTokens: delta.input,
|
|
141
|
+
cacheReadTokens: delta.cacheRead,
|
|
142
|
+
cacheWriteTokens: delta.cacheWrite,
|
|
143
|
+
outputTokens: delta.output,
|
|
144
|
+
reasoningTokens: delta.reasoning,
|
|
145
|
+
reasoningIncludedInOutput: usage.reasoningIncludedInOutput,
|
|
146
|
+
cacheInclusiveInputTokens,
|
|
147
|
+
billableOutputTokens,
|
|
148
|
+
totalTokens: cacheInclusiveInputTokens + billableOutputTokens,
|
|
149
|
+
...(usage.model ? { model: usage.model } : {}),
|
|
150
|
+
provenance: usage.provenance,
|
|
151
|
+
};
|
|
152
|
+
const messageUsage = {
|
|
153
|
+
input_tokens: delta.input,
|
|
154
|
+
output_tokens: delta.output,
|
|
155
|
+
cache_creation_input_tokens: delta.cacheWrite,
|
|
156
|
+
cache_read_input_tokens: delta.cacheRead,
|
|
157
|
+
...(delta.reasoning ? { reasoning_tokens: delta.reasoning } : {}),
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
...event,
|
|
161
|
+
message: { ...message, usage: messageUsage, normalizedUsage, usageKind: 'correction' },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Whether a message id has already been counted by this deduper. */
|
|
165
|
+
has(id) {
|
|
166
|
+
return this.seen.has(id);
|
|
167
|
+
}
|
|
168
|
+
reset() {
|
|
169
|
+
this.seen.clear();
|
|
170
|
+
}
|
|
171
|
+
remember(id, buckets) {
|
|
172
|
+
this.seen.delete(id);
|
|
173
|
+
this.seen.set(id, buckets);
|
|
174
|
+
if (this.seen.size > this.maxKeys) {
|
|
175
|
+
const oldest = this.seen.keys().next().value;
|
|
176
|
+
if (oldest !== undefined)
|
|
177
|
+
this.seen.delete(oldest);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.ClaudeUsageDeduper = ClaudeUsageDeduper;
|
|
182
|
+
/**
|
|
183
|
+
* Deduplicated normalized usage for one raw Claude Code JSONL assistant line,
|
|
184
|
+
* or null when the line carries none (or only repeats usage already counted).
|
|
185
|
+
*/
|
|
186
|
+
function dedupedRawClaudeUsage(raw, deduper) {
|
|
187
|
+
if (!raw || raw.type !== 'assistant' || !raw.message?.usage)
|
|
188
|
+
return null;
|
|
189
|
+
const event = deduper.apply(normalizeClaudeUsage({
|
|
190
|
+
type: 'assistant',
|
|
191
|
+
timestamp: raw.timestamp ?? '',
|
|
192
|
+
message: raw.message,
|
|
193
|
+
}));
|
|
194
|
+
return event.message?.normalizedUsage ?? null;
|
|
195
|
+
}
|
|
@@ -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
|
-
/**
|
|
21
|
-
|
|
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
|
|
@@ -69,6 +78,13 @@ export interface CollectUsageEventsOptions {
|
|
|
69
78
|
/** Skip the on-disk cache entirely. */
|
|
70
79
|
noCache?: boolean;
|
|
71
80
|
onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Stops the collection before listing and between sessions; the promise
|
|
83
|
+
* rejects with the signal's reason (an `AbortError` by default). Sessions
|
|
84
|
+
* read before the abort keep their cache entries; no partial session is
|
|
85
|
+
* cached.
|
|
86
|
+
*/
|
|
87
|
+
signal?: AbortSignal;
|
|
72
88
|
}
|
|
73
89
|
export interface CollectUsageEventsResult {
|
|
74
90
|
/** Events within the window, oldest first. */
|
|
@@ -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
|
-
/**
|
|
63
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
@@ -220,12 +290,15 @@ async function collectUsageEvents(options) {
|
|
|
220
290
|
const untilMs = toMs(options.until, 'until');
|
|
221
291
|
const cacheDir = options.cacheDir ?? getUsageCacheDir();
|
|
222
292
|
const useCache = !options.noCache;
|
|
293
|
+
const signal = options.signal;
|
|
294
|
+
signal?.throwIfAborted();
|
|
223
295
|
const listed = await (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
|
|
224
296
|
since: options.since,
|
|
225
297
|
workspacePath: options.workspacePath,
|
|
226
298
|
limit: Number.MAX_SAFE_INTEGER,
|
|
227
299
|
concurrency: options.concurrency,
|
|
228
300
|
});
|
|
301
|
+
signal?.throwIfAborted();
|
|
229
302
|
const diagnostics = [...listed.diagnostics];
|
|
230
303
|
for (const diagnostic of listed.diagnostics)
|
|
231
304
|
options.onDiagnostic?.(diagnostic);
|
|
@@ -236,10 +309,12 @@ async function collectUsageEvents(options) {
|
|
|
236
309
|
let cacheMisses = 0;
|
|
237
310
|
let wroteCache = false;
|
|
238
311
|
for (const preview of listed.previews) {
|
|
312
|
+
signal?.throwIfAborted();
|
|
239
313
|
const provider = providerById.get(preview.provider);
|
|
240
314
|
if (!provider)
|
|
241
315
|
continue;
|
|
242
|
-
const
|
|
316
|
+
const subagentFiles = claudeSubagentFiles(preview);
|
|
317
|
+
const fingerprint = sessionFingerprint(preview, subagentFiles);
|
|
243
318
|
const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
|
|
244
319
|
let cachedEvents = null;
|
|
245
320
|
let project = preview.workspacePath;
|
|
@@ -257,7 +332,7 @@ async function collectUsageEvents(options) {
|
|
|
257
332
|
if (!cachedEvents) {
|
|
258
333
|
cacheMisses += 1;
|
|
259
334
|
try {
|
|
260
|
-
cachedEvents = readSessionUsage(provider, preview);
|
|
335
|
+
cachedEvents = readSessionUsage(provider, preview, subagentFiles);
|
|
261
336
|
}
|
|
262
337
|
catch (error) {
|
|
263
338
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
});
|
package/dist/watchers/types.d.ts
CHANGED
|
@@ -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;
|