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.
- package/CHANGELOG.md +86 -0
- package/README.md +188 -27
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +16 -6
- package/dist/browser.js +24 -2
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +5 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +11 -6
- package/dist/esm/modelCatalog.js +7 -5
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +425 -47
- package/dist/esm/node.js +9 -3
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +351 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +85 -22
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +17 -9
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +84 -62
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +46 -3
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +442 -58
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.js +44 -12
- package/dist/modelCatalog.js +7 -5
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +434 -49
- package/dist/node.d.ts +16 -5
- package/dist/node.js +33 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +118 -0
- package/dist/parsers/codexParser.js +352 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +88 -22
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +16 -8
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +85 -64
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/providers/types.d.ts +14 -0
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +46 -3
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/eventBridge.js +15 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +124 -4
- package/dist/watchers/jsonlTail.js +446 -59
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +41 -1
|
@@ -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.
|
|
16
|
-
* are top-level rollouts, already listed
|
|
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
|
*
|
|
@@ -21,7 +31,10 @@
|
|
|
21
31
|
*/
|
|
22
32
|
import * as fs from 'fs';
|
|
23
33
|
import * as path from 'path';
|
|
34
|
+
import { codexRolloutOrigin } from '../parsers/codexRolloutOrigin';
|
|
24
35
|
import { getConfigDir } from '../paths';
|
|
36
|
+
import { readPromptOrdinalMarks } from '../promptHistory';
|
|
37
|
+
import { readFirstLineAsync } from '../providers/codexRollouts';
|
|
25
38
|
import { listSessionPreviewsAsync } from '../sessionPreviews';
|
|
26
39
|
import { calculateNormalizedUsageCost, extractNormalizedUsage } from '../usageNormalization';
|
|
27
40
|
import { atomicWriteJson } from '../writers/atomic';
|
|
@@ -29,9 +42,11 @@ import { resolveWorkspaceRoots } from '../workspaceRoots';
|
|
|
29
42
|
/**
|
|
30
43
|
* Bump when the cached record shape or the counting rules change; older files
|
|
31
44
|
* are re-read. v2: Claude split lines and duplicate Codex `token_count` events
|
|
32
|
-
* are counted once, and Claude subagent transcripts are included.
|
|
45
|
+
* are counted once, and Claude subagent transcripts are included. v3: events
|
|
46
|
+
* keep reasoning tokens, prompt ordinal, message id, and subagent, and files
|
|
47
|
+
* move to a versioned name (`<provider>--<session>.v3.json`).
|
|
33
48
|
*/
|
|
34
|
-
export const USAGE_CACHE_VERSION =
|
|
49
|
+
export const USAGE_CACHE_VERSION = 3;
|
|
35
50
|
/**
|
|
36
51
|
* Most-recently-used cache files kept on disk. Sized for a busy month of
|
|
37
52
|
* sessions (files are a few KB each); a collection larger than this still
|
|
@@ -42,9 +57,15 @@ export const MAX_USAGE_CACHE_FILES = 5000;
|
|
|
42
57
|
export function getUsageCacheDir() {
|
|
43
58
|
return path.join(getConfigDir(), 'usage-cache');
|
|
44
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Cache file of one session. The name carries the cache version, so a reader
|
|
62
|
+
* of another version sharing the directory (which rejects this version, and
|
|
63
|
+
* versions before 3 use the unversioned `<provider>--<session>.json`) never
|
|
64
|
+
* overwrites it, and this version never overwrites theirs.
|
|
65
|
+
*/
|
|
45
66
|
function cacheFilePath(dir, provider, sessionId) {
|
|
46
67
|
const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
47
|
-
return path.join(dir, `${provider}--${safe}.json`);
|
|
68
|
+
return path.join(dir, `${provider}--${safe}.v${USAGE_CACHE_VERSION}.json`);
|
|
48
69
|
}
|
|
49
70
|
function toMs(value, label) {
|
|
50
71
|
if (value === undefined)
|
|
@@ -75,7 +96,11 @@ function readCache(filePath) {
|
|
|
75
96
|
return null;
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Deletes all but the `keep` most recently used cache files (by mtime), of
|
|
101
|
+
* every cache version alike, so files no reader touches any more age out.
|
|
102
|
+
* Best-effort.
|
|
103
|
+
*/
|
|
79
104
|
export function pruneUsageCache(keep = MAX_USAGE_CACHE_FILES, dir = getUsageCacheDir()) {
|
|
80
105
|
try {
|
|
81
106
|
const entries = fs
|
|
@@ -108,7 +133,7 @@ export function pruneUsageCache(keep = MAX_USAGE_CACHE_FILES, dir = getUsageCach
|
|
|
108
133
|
return 0;
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
|
-
function cachedEventFromUsage(usage, timestamp, model) {
|
|
136
|
+
function cachedEventFromUsage(usage, timestamp, model, messageId, agentId) {
|
|
112
137
|
const record = {
|
|
113
138
|
t: timestamp,
|
|
114
139
|
m: model,
|
|
@@ -121,16 +146,29 @@ function cachedEventFromUsage(usage, timestamp, model) {
|
|
|
121
146
|
};
|
|
122
147
|
if (usage.reportedCostUsd !== undefined)
|
|
123
148
|
record.rc = usage.reportedCostUsd;
|
|
149
|
+
if (usage.reasoningTokens > 0)
|
|
150
|
+
record.rt = usage.reasoningTokens;
|
|
151
|
+
if (messageId && !messageId.startsWith('token-count-'))
|
|
152
|
+
record.rid = messageId;
|
|
153
|
+
if (agentId)
|
|
154
|
+
record.sa = agentId;
|
|
124
155
|
return record;
|
|
125
156
|
}
|
|
126
|
-
/**
|
|
127
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Price a cached event with the current catalog (or its provider-reported
|
|
159
|
+
* cost). Output is priced from the billable output (`bo`), which already
|
|
160
|
+
* holds reasoning however the provider reports it, so restoring the reasoning
|
|
161
|
+
* count never changes the cost.
|
|
162
|
+
*/
|
|
163
|
+
function priceCachedEvent(cached, provider, sessionId, project, sessionFile, sessionSubagent) {
|
|
164
|
+
const reasoningTokens = typeof cached.rt === 'number' && cached.rt > 0 ? cached.rt : 0;
|
|
128
165
|
const usage = {
|
|
129
166
|
uncachedInputTokens: cached.i,
|
|
130
167
|
cacheReadTokens: cached.cr,
|
|
131
168
|
cacheWriteTokens: cached.cw,
|
|
132
169
|
outputTokens: cached.o,
|
|
133
|
-
reasoningTokens
|
|
170
|
+
reasoningTokens,
|
|
171
|
+
// `bo` is already the billable output; nothing is added to it.
|
|
134
172
|
reasoningIncludedInOutput: true,
|
|
135
173
|
cacheInclusiveInputTokens: cached.i + cached.cr + cached.cw,
|
|
136
174
|
billableOutputTokens: cached.bo,
|
|
@@ -142,6 +180,9 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
|
|
|
142
180
|
usage,
|
|
143
181
|
modelId: cached.m === 'unknown' ? undefined : cached.m,
|
|
144
182
|
});
|
|
183
|
+
const subagent = typeof cached.sa === 'string' && cached.sa
|
|
184
|
+
? { agentId: cached.sa, parentSessionId: sessionId }
|
|
185
|
+
: sessionSubagent;
|
|
145
186
|
return {
|
|
146
187
|
timestamp: cached.t,
|
|
147
188
|
provider,
|
|
@@ -154,13 +195,19 @@ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
|
|
|
154
195
|
cacheWriteTokens: cached.cw,
|
|
155
196
|
cacheReadTokens: cached.cr,
|
|
156
197
|
totalTokens: cached.tt,
|
|
198
|
+
...(reasoningTokens > 0 ? { reasoningTokens } : {}),
|
|
157
199
|
},
|
|
158
200
|
costUsd: priced.costUsd,
|
|
159
201
|
costProvenance: priced.source,
|
|
160
202
|
filePath: sessionFile,
|
|
203
|
+
...(cached.po === null || (typeof cached.po === 'number' && Number.isInteger(cached.po))
|
|
204
|
+
? { promptOrdinal: cached.po }
|
|
205
|
+
: {}),
|
|
206
|
+
...(typeof cached.rid === 'string' ? { requestId: cached.rid } : {}),
|
|
207
|
+
...(subagent ? { subagent: { ...subagent } } : {}),
|
|
161
208
|
};
|
|
162
209
|
}
|
|
163
|
-
const CLAUDE_AGENT_FILE = /^agent
|
|
210
|
+
const CLAUDE_AGENT_FILE = /^agent-(.+)\.jsonl$/;
|
|
164
211
|
/** Claude Code subagent transcripts of a session, sorted by name. */
|
|
165
212
|
function claudeSubagentFiles(preview) {
|
|
166
213
|
if (preview.provider !== 'claude-code')
|
|
@@ -180,6 +227,7 @@ function claudeSubagentFiles(preview) {
|
|
|
180
227
|
files.push({
|
|
181
228
|
path: path.join(dir, name),
|
|
182
229
|
name,
|
|
230
|
+
agentId: CLAUDE_AGENT_FILE.exec(name)?.[1] ?? name,
|
|
183
231
|
sizeBytes: stat.size,
|
|
184
232
|
modifiedAt: stat.mtimeMs,
|
|
185
233
|
});
|
|
@@ -196,7 +244,7 @@ function sessionFingerprint(preview, subagents) {
|
|
|
196
244
|
return base;
|
|
197
245
|
return `${base}|${subagents.map((f) => `${f.name}:${f.sizeBytes}:${f.modifiedAt}`).join(',')}`;
|
|
198
246
|
}
|
|
199
|
-
function readTranscriptUsage(provider, filePath, cached) {
|
|
247
|
+
function readTranscriptUsage(provider, filePath, cached, agentId) {
|
|
200
248
|
const reader = provider.createReader(filePath);
|
|
201
249
|
const events = reader.readAll();
|
|
202
250
|
reader.flush();
|
|
@@ -218,12 +266,14 @@ function readTranscriptUsage(provider, filePath, cached) {
|
|
|
218
266
|
target.cr += usage.cacheReadTokens;
|
|
219
267
|
target.bo += usage.billableOutputTokens;
|
|
220
268
|
target.tt += usage.totalTokens;
|
|
269
|
+
if (usage.reasoningTokens > 0)
|
|
270
|
+
target.rt = (target.rt ?? 0) + usage.reasoningTokens;
|
|
221
271
|
continue;
|
|
222
272
|
}
|
|
223
273
|
const timestamp = Date.parse(event.timestamp);
|
|
224
274
|
if (!Number.isFinite(timestamp))
|
|
225
275
|
continue;
|
|
226
|
-
const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown');
|
|
276
|
+
const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown', messageId, agentId);
|
|
227
277
|
cached.push(record);
|
|
228
278
|
if (messageId)
|
|
229
279
|
byMessageId.set(messageId, record);
|
|
@@ -233,10 +283,75 @@ function readSessionUsage(provider, preview, subagents) {
|
|
|
233
283
|
const cached = [];
|
|
234
284
|
readTranscriptUsage(provider, preview.filePath, cached);
|
|
235
285
|
for (const file of subagents)
|
|
236
|
-
readTranscriptUsage(provider, file.path, cached);
|
|
286
|
+
readTranscriptUsage(provider, file.path, cached, file.agentId);
|
|
237
287
|
cached.sort((a, b) => a.t - b.t);
|
|
238
288
|
return cached;
|
|
239
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Sets each event's `po` to the highest ordinal among the prompts timestamped
|
|
292
|
+
* at or before it, or null when none is. With prompts in time order (the
|
|
293
|
+
* usual case) that is the last prompt before the event.
|
|
294
|
+
*/
|
|
295
|
+
function assignPromptOrdinals(events, marks) {
|
|
296
|
+
const sorted = [...marks].sort((a, b) => a.timestampMs - b.timestampMs || a.ordinal - b.ordinal);
|
|
297
|
+
const highest = [];
|
|
298
|
+
let max = -1;
|
|
299
|
+
for (const mark of sorted) {
|
|
300
|
+
max = Math.max(max, mark.ordinal);
|
|
301
|
+
highest.push(max);
|
|
302
|
+
}
|
|
303
|
+
for (const event of events) {
|
|
304
|
+
let low = 0;
|
|
305
|
+
let high = sorted.length;
|
|
306
|
+
while (low < high) {
|
|
307
|
+
const middle = (low + high) >>> 1;
|
|
308
|
+
if (sorted[middle].timestampMs <= event.t)
|
|
309
|
+
low = middle + 1;
|
|
310
|
+
else
|
|
311
|
+
high = middle;
|
|
312
|
+
}
|
|
313
|
+
event.po = low === 0 ? null : highest[low - 1];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Prompt ordinals for a freshly read session, from its main transcript.
|
|
318
|
+
* Returns false when the transcript could not be scanned (the events then
|
|
319
|
+
* keep no ordinal and the session is not cached, so the next run retries).
|
|
320
|
+
*/
|
|
321
|
+
async function addPromptOrdinals(preview, events) {
|
|
322
|
+
if (preview.provider !== 'claude-code' && preview.provider !== 'codex')
|
|
323
|
+
return true;
|
|
324
|
+
let marks;
|
|
325
|
+
try {
|
|
326
|
+
marks = await readPromptOrdinalMarks(preview.provider, preview.sessionId, preview.filePath);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
assignPromptOrdinals(events, marks);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
/** Parent of a Codex rollout that is a fork or a spawned subagent, from its first line only. */
|
|
335
|
+
async function codexSessionSubagent(preview) {
|
|
336
|
+
if (preview.provider !== 'codex')
|
|
337
|
+
return undefined;
|
|
338
|
+
const { line } = await readFirstLineAsync(preview.filePath);
|
|
339
|
+
if (!line)
|
|
340
|
+
return undefined;
|
|
341
|
+
let first;
|
|
342
|
+
try {
|
|
343
|
+
first = JSON.parse(line);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
const origin = codexRolloutOrigin(first);
|
|
349
|
+
if (!origin.parentId || (!origin.isSubagent && !origin.forkedFromId))
|
|
350
|
+
return undefined;
|
|
351
|
+
if (origin.parentId === preview.sessionId)
|
|
352
|
+
return undefined;
|
|
353
|
+
return { agentId: preview.sessionId, parentSessionId: origin.parentId };
|
|
354
|
+
}
|
|
240
355
|
function yieldToEventLoop() {
|
|
241
356
|
return new Promise((resolve) => setImmediate(resolve));
|
|
242
357
|
}
|
|
@@ -276,12 +391,14 @@ export async function collectUsageEvents(options) {
|
|
|
276
391
|
const fingerprint = sessionFingerprint(preview, subagentFiles);
|
|
277
392
|
const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
|
|
278
393
|
let cachedEvents = null;
|
|
394
|
+
let sessionSubagent;
|
|
279
395
|
let project = preview.workspacePath;
|
|
280
396
|
let fromCache = false;
|
|
281
397
|
if (useCache) {
|
|
282
398
|
const cached = readCache(filePath);
|
|
283
399
|
if (cached && cached.fingerprint === fingerprint && cached.sessionId === preview.sessionId) {
|
|
284
400
|
cachedEvents = cached.events;
|
|
401
|
+
sessionSubagent = validSubagent(cached.subagent);
|
|
285
402
|
project = project ?? cached.project;
|
|
286
403
|
fromCache = true;
|
|
287
404
|
cacheHits += 1;
|
|
@@ -305,7 +422,10 @@ export async function collectUsageEvents(options) {
|
|
|
305
422
|
options.onDiagnostic?.(diagnostic);
|
|
306
423
|
continue;
|
|
307
424
|
}
|
|
308
|
-
|
|
425
|
+
// Ordinals and origin are best-effort: a failure costs only the cache write.
|
|
426
|
+
const ordinalsRead = await addPromptOrdinals(preview, cachedEvents);
|
|
427
|
+
sessionSubagent = await codexSessionSubagent(preview).catch(() => undefined);
|
|
428
|
+
if (useCache && ordinalsRead) {
|
|
309
429
|
const record = {
|
|
310
430
|
version: USAGE_CACHE_VERSION,
|
|
311
431
|
provider: preview.provider,
|
|
@@ -313,6 +433,7 @@ export async function collectUsageEvents(options) {
|
|
|
313
433
|
fingerprint,
|
|
314
434
|
project,
|
|
315
435
|
events: cachedEvents,
|
|
436
|
+
...(sessionSubagent ? { subagent: sessionSubagent } : {}),
|
|
316
437
|
};
|
|
317
438
|
try {
|
|
318
439
|
await atomicWriteJson(filePath, record);
|
|
@@ -332,7 +453,7 @@ export async function collectUsageEvents(options) {
|
|
|
332
453
|
continue;
|
|
333
454
|
if (untilMs !== null && cached.t > untilMs)
|
|
334
455
|
continue;
|
|
335
|
-
events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath));
|
|
456
|
+
events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath, sessionSubagent));
|
|
336
457
|
first = first === null ? cached.t : Math.min(first, cached.t);
|
|
337
458
|
last = last === null ? cached.t : Math.max(last, cached.t);
|
|
338
459
|
count += 1;
|
|
@@ -361,6 +482,14 @@ export async function collectUsageEvents(options) {
|
|
|
361
482
|
...(scope ? { roots: scope.roots } : {}),
|
|
362
483
|
};
|
|
363
484
|
}
|
|
485
|
+
function validSubagent(value) {
|
|
486
|
+
if (typeof value !== 'object' || value === null)
|
|
487
|
+
return undefined;
|
|
488
|
+
const { agentId, parentSessionId } = value;
|
|
489
|
+
return typeof agentId === 'string' && typeof parentSessionId === 'string'
|
|
490
|
+
? { agentId, parentSessionId }
|
|
491
|
+
: undefined;
|
|
492
|
+
}
|
|
364
493
|
/** The roots to list, or undefined for an unscoped (or legacy single-root) call. */
|
|
365
494
|
function usageScope(options) {
|
|
366
495
|
if (options.workspacePaths) {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { classifyCostProvenance } from '../aggregation/costProvenance';
|
|
9
9
|
import { formatLocalDateKey } from '../formatting';
|
|
10
|
+
import { canonicalModelId } from '../modelInfo';
|
|
11
|
+
import { SYNTHETIC_MODEL } from './claudeUsageDedupe';
|
|
10
12
|
function dayParts(ms, utc) {
|
|
11
13
|
const date = new Date(ms);
|
|
12
14
|
return utc
|
|
@@ -69,7 +71,8 @@ function addTokens(target, source) {
|
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* Group usage events into rows. Rows are sorted by key (sessions by first
|
|
72
|
-
* event), then provider, project, and model, so tables read oldest first
|
|
74
|
+
* event), then provider, project, and model, so tables read oldest first;
|
|
75
|
+
* rows grouped by `session` within one of those follow their first event.
|
|
73
76
|
*/
|
|
74
77
|
export function bucketUsage(events, options) {
|
|
75
78
|
const groupBy = new Set(options.groupBy ?? ['provider']);
|
|
@@ -77,13 +80,21 @@ export function bucketUsage(events, options) {
|
|
|
77
80
|
const byProvider = isSession || groupBy.has('provider');
|
|
78
81
|
const byProject = isSession || groupBy.has('project');
|
|
79
82
|
const byModel = groupBy.has('model');
|
|
83
|
+
const bySession = isSession || groupBy.has('session');
|
|
84
|
+
const keyModel = options.modelKey === 'canonical'
|
|
85
|
+
? (model) => (model === SYNTHETIC_MODEL ? model : canonicalModelId(model) || model)
|
|
86
|
+
: (model) => model;
|
|
80
87
|
const buckets = new Map();
|
|
81
88
|
for (const event of events) {
|
|
89
|
+
if (options.excludeSynthetic && event.model === SYNTHETIC_MODEL)
|
|
90
|
+
continue;
|
|
82
91
|
const key = usageBucketKey(event, options);
|
|
83
92
|
const provider = byProvider ? event.provider : null;
|
|
84
93
|
const project = byProject ? event.project : null;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
94
|
+
const eventModel = keyModel(event.model);
|
|
95
|
+
const model = byModel ? eventModel : null;
|
|
96
|
+
const sessionId = bySession ? event.sessionId : null;
|
|
97
|
+
const id = JSON.stringify([key, provider, project, model, sessionId]);
|
|
87
98
|
let acc = buckets.get(id);
|
|
88
99
|
if (!acc) {
|
|
89
100
|
acc = {
|
|
@@ -93,15 +104,17 @@ export function bucketUsage(events, options) {
|
|
|
93
104
|
provider,
|
|
94
105
|
project,
|
|
95
106
|
model,
|
|
96
|
-
sessionId
|
|
107
|
+
sessionId,
|
|
97
108
|
models: [],
|
|
98
109
|
calls: 0,
|
|
99
110
|
sessions: 0,
|
|
100
111
|
tokens: emptyTokens(),
|
|
101
112
|
totalTokens: 0,
|
|
102
113
|
costUsd: 0,
|
|
114
|
+
pricedCostUsd: null,
|
|
103
115
|
costProvenance: 'none',
|
|
104
116
|
unpricedCalls: 0,
|
|
117
|
+
reasoningTokens: 0,
|
|
105
118
|
firstTimestamp: event.timestamp,
|
|
106
119
|
lastTimestamp: event.timestamp,
|
|
107
120
|
},
|
|
@@ -116,12 +129,14 @@ export function bucketUsage(events, options) {
|
|
|
116
129
|
row.calls += 1;
|
|
117
130
|
addTokens(row.tokens, event.tokens);
|
|
118
131
|
row.totalTokens += event.tokens.totalTokens;
|
|
132
|
+
row.reasoningTokens = (row.reasoningTokens ?? 0) + reasoningOf(event.tokens.reasoningTokens);
|
|
119
133
|
row.firstTimestamp = Math.min(row.firstTimestamp, event.timestamp);
|
|
120
134
|
row.lastTimestamp = Math.max(row.lastTimestamp, event.timestamp);
|
|
121
135
|
acc.sessionIds.add(event.sessionId);
|
|
122
|
-
acc.modelTokens.set(
|
|
136
|
+
acc.modelTokens.set(eventModel, (acc.modelTokens.get(eventModel) ?? 0) + event.tokens.totalTokens);
|
|
123
137
|
if (event.costUsd !== null) {
|
|
124
138
|
row.costUsd += event.costUsd;
|
|
139
|
+
row.pricedCostUsd = (row.pricedCostUsd ?? 0) + event.costUsd;
|
|
125
140
|
if (event.costProvenance === 'provider-reported')
|
|
126
141
|
acc.reportedCalls += 1;
|
|
127
142
|
else
|
|
@@ -147,7 +162,14 @@ export function bucketUsage(events, options) {
|
|
|
147
162
|
return rows.sort((a, b) => (isSession ? a.firstTimestamp - b.firstTimestamp : a.key.localeCompare(b.key)) ||
|
|
148
163
|
(a.provider ?? '').localeCompare(b.provider ?? '') ||
|
|
149
164
|
(a.project ?? '').localeCompare(b.project ?? '') ||
|
|
150
|
-
(a.model ?? '').localeCompare(b.model ?? '')
|
|
165
|
+
(a.model ?? '').localeCompare(b.model ?? '') ||
|
|
166
|
+
(bySession && !isSession
|
|
167
|
+
? a.firstTimestamp - b.firstTimestamp ||
|
|
168
|
+
(a.sessionId ?? '').localeCompare(b.sessionId ?? '')
|
|
169
|
+
: 0));
|
|
170
|
+
}
|
|
171
|
+
function reasoningOf(value) {
|
|
172
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
151
173
|
}
|
|
152
174
|
/**
|
|
153
175
|
* Sum rows that do not overlap (for example every row of one `bucketUsage`
|
|
@@ -161,8 +183,10 @@ export function summarizeUsageRows(rows, events) {
|
|
|
161
183
|
tokens: emptyTokens(),
|
|
162
184
|
totalTokens: 0,
|
|
163
185
|
costUsd: 0,
|
|
186
|
+
pricedCostUsd: null,
|
|
164
187
|
costProvenance: 'none',
|
|
165
188
|
unpricedCalls: 0,
|
|
189
|
+
reasoningTokens: 0,
|
|
166
190
|
};
|
|
167
191
|
let reportedCalls = 0;
|
|
168
192
|
let estimatedCalls = 0;
|
|
@@ -172,6 +196,15 @@ export function summarizeUsageRows(rows, events) {
|
|
|
172
196
|
addTokens(totals.tokens, row.tokens);
|
|
173
197
|
totals.totalTokens += row.totalTokens;
|
|
174
198
|
totals.costUsd += row.costUsd;
|
|
199
|
+
// Rows from before `pricedCostUsd` existed: any priced call means a priced cost.
|
|
200
|
+
const rowPriced = row.pricedCostUsd !== undefined
|
|
201
|
+
? row.pricedCostUsd
|
|
202
|
+
: row.calls > row.unpricedCalls
|
|
203
|
+
? row.costUsd
|
|
204
|
+
: null;
|
|
205
|
+
if (rowPriced !== null)
|
|
206
|
+
totals.pricedCostUsd = (totals.pricedCostUsd ?? 0) + rowPriced;
|
|
207
|
+
totals.reasoningTokens = (totals.reasoningTokens ?? 0) + reasoningOf(row.reasoningTokens);
|
|
175
208
|
totals.unpricedCalls += row.unpricedCalls;
|
|
176
209
|
const priced = row.calls - row.unpricedCalls;
|
|
177
210
|
if (row.costProvenance === 'reported')
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* FollowEvent is a lossy summary for timeline display and CLI output.
|
|
9
9
|
*/
|
|
10
10
|
import { formatToolSummary } from '../formatters/toolSummary';
|
|
11
|
+
import { isTaskNotificationSystemEvent, taskNotificationFromEvent } from '../subagentSpawn';
|
|
11
12
|
/**
|
|
12
13
|
* Converts a single SessionEvent to one or more FollowEvents.
|
|
13
14
|
*
|
|
@@ -148,6 +149,20 @@ export function toFollowEvents(event, providerId) {
|
|
|
148
149
|
break;
|
|
149
150
|
}
|
|
150
151
|
case 'system': {
|
|
152
|
+
// A background task's notice (opt-in reader event): its summary, not the raw block.
|
|
153
|
+
const notification = isTaskNotificationSystemEvent(event)
|
|
154
|
+
? taskNotificationFromEvent(event)
|
|
155
|
+
: null;
|
|
156
|
+
if (notification) {
|
|
157
|
+
events.push({
|
|
158
|
+
providerId,
|
|
159
|
+
type: 'system',
|
|
160
|
+
timestamp: ts,
|
|
161
|
+
summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
|
|
162
|
+
raw: event,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
151
166
|
const text = extractTextContent(event.message?.content);
|
|
152
167
|
const label = event.message?.sourceLabel || event.message?.role || 'system';
|
|
153
168
|
const summary = text ||
|
|
@@ -41,13 +41,15 @@ export function resolveSessionPath(provider, workspacePath, sessionId) {
|
|
|
41
41
|
export function createWatcher(options) {
|
|
42
42
|
const { provider, workspacePath, sessionId, callbacks } = options;
|
|
43
43
|
const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
|
|
44
|
-
const watcher = createWatcherForProvider(provider, sessionPath, callbacks
|
|
44
|
+
const watcher = createWatcherForProvider(provider, sessionPath, callbacks, {
|
|
45
|
+
includeTaskNotifications: options.includeTaskNotifications === true,
|
|
46
|
+
});
|
|
45
47
|
return { watcher, sessionPath };
|
|
46
48
|
}
|
|
47
|
-
function createWatcherForProvider(provider, sessionPath, callbacks) {
|
|
49
|
+
function createWatcherForProvider(provider, sessionPath, callbacks, jsonlOptions) {
|
|
48
50
|
switch (provider.id) {
|
|
49
51
|
case 'claude-code':
|
|
50
|
-
return new JsonlSessionWatcher(provider.id, sessionPath, callbacks);
|
|
52
|
+
return new JsonlSessionWatcher(provider.id, sessionPath, callbacks, jsonlOptions);
|
|
51
53
|
case 'codex':
|
|
52
54
|
return new ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
|
|
53
55
|
case 'opencode': {
|