sidekick-shared 0.27.2 → 0.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +24 -3
- 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 +2 -0
- package/dist/node.js +8 -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 +11 -2
- package/dist/usage/usageEvents.js +79 -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
|
@@ -52,6 +52,9 @@ const fs = __importStar(require("fs"));
|
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
53
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
54
54
|
const noiseClassifier_1 = require("../formatters/noiseClassifier");
|
|
55
|
+
const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
|
|
56
|
+
const subagentUsage_1 = require("../usage/subagentUsage");
|
|
57
|
+
const subagentScanner_1 = require("./subagentScanner");
|
|
55
58
|
// ── Constants ──
|
|
56
59
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
57
60
|
// ── Public API ──
|
|
@@ -101,8 +104,8 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
101
104
|
const toolCalls = [];
|
|
102
105
|
let agentType;
|
|
103
106
|
let description;
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
108
|
+
const usageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
|
|
106
109
|
let startTime;
|
|
107
110
|
let endTime;
|
|
108
111
|
for (const line of lines) {
|
|
@@ -153,11 +156,10 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
153
156
|
isHardNoise: hardNoise,
|
|
154
157
|
});
|
|
155
158
|
// Extract token usage
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
159
|
+
// Split lines of one response repeat its usage; count each response once.
|
|
160
|
+
const usage = (0, claudeUsageDedupe_1.dedupedRawClaudeUsage)(raw, usageDeduper);
|
|
161
|
+
if (usage)
|
|
162
|
+
(0, subagentUsage_1.addUsageToSubagent)(tokens, usage);
|
|
161
163
|
// Extract tool calls
|
|
162
164
|
if (raw.type === 'assistant' && Array.isArray(raw.message?.content)) {
|
|
163
165
|
for (const block of raw.message.content) {
|
|
@@ -194,7 +196,10 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
194
196
|
// Skip malformed lines
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
|
-
|
|
199
|
+
const meta = (0, subagentScanner_1.readAgentMeta)(filePath);
|
|
200
|
+
agentType = meta.agentType ?? agentType;
|
|
201
|
+
description = meta.description ?? description;
|
|
202
|
+
if (events.length === 0 && !agentType && !description && !tokens.totalTokens) {
|
|
198
203
|
return null;
|
|
199
204
|
}
|
|
200
205
|
const durationMs = startTime && endTime ? endTime.getTime() - startTime.getTime() : undefined;
|
|
@@ -209,8 +214,12 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
209
214
|
agentType,
|
|
210
215
|
description,
|
|
211
216
|
toolCalls,
|
|
212
|
-
inputTokens,
|
|
213
|
-
outputTokens,
|
|
217
|
+
inputTokens: tokens.inputTokens,
|
|
218
|
+
outputTokens: tokens.outputTokens,
|
|
219
|
+
cacheReadTokens: tokens.cacheReadTokens ?? 0,
|
|
220
|
+
cacheWriteTokens: tokens.cacheWriteTokens ?? 0,
|
|
221
|
+
reasoningTokens: tokens.reasoningTokens ?? 0,
|
|
222
|
+
totalTokens: tokens.totalTokens ?? 0,
|
|
214
223
|
startTime,
|
|
215
224
|
endTime,
|
|
216
225
|
durationMs,
|
|
@@ -50,13 +50,13 @@ const string_decoder_1 = require("string_decoder");
|
|
|
50
50
|
const sessionContext_1 = require("../context/sessionContext");
|
|
51
51
|
const sessionStats_1 = require("../sessionStats");
|
|
52
52
|
const jsonl_1 = require("../parsers/jsonl");
|
|
53
|
+
const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
|
|
53
54
|
const diagnostics_1 = require("./diagnostics");
|
|
54
55
|
const directoryListingCache_1 = require("./directoryListingCache");
|
|
55
56
|
const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
|
|
56
57
|
const subagentScanner_1 = require("../parsers/subagentScanner");
|
|
57
58
|
const modelContext_1 = require("../modelContext");
|
|
58
59
|
const sessionEvent_1 = require("../schemas/sessionEvent");
|
|
59
|
-
const usageNormalization_1 = require("../usageNormalization");
|
|
60
60
|
/** Type guard for content blocks with a `type` string property */
|
|
61
61
|
function isTypedBlock(block) {
|
|
62
62
|
return (block !== null &&
|
|
@@ -163,12 +163,15 @@ class ClaudeCodeReader {
|
|
|
163
163
|
events = [];
|
|
164
164
|
_wasTruncated = false;
|
|
165
165
|
decoder = new string_decoder_1.StringDecoder('utf8');
|
|
166
|
-
|
|
166
|
+
/** Split lines of one response repeat its usage; count each response once. */
|
|
167
|
+
usageDeduper;
|
|
168
|
+
constructor(sessionPath, usageDeduper) {
|
|
167
169
|
this.sessionPath = sessionPath;
|
|
170
|
+
this.usageDeduper = usageDeduper ?? new claudeUsageDedupe_1.ClaudeUsageDeduper();
|
|
168
171
|
this.parser = new jsonl_1.JsonlParser({
|
|
169
172
|
onEvent: (raw) => {
|
|
170
173
|
for (const event of (0, sessionEvent_1.extractSessionEvents)(raw)) {
|
|
171
|
-
this.events.push(normalizeClaudeUsage(event));
|
|
174
|
+
this.events.push(this.usageDeduper.apply((0, claudeUsageDedupe_1.normalizeClaudeUsage)(event)));
|
|
172
175
|
}
|
|
173
176
|
},
|
|
174
177
|
onError: (_err, _line) => {
|
|
@@ -192,6 +195,7 @@ class ClaudeCodeReader {
|
|
|
192
195
|
this.committedPosition = 0;
|
|
193
196
|
this.parser.reset();
|
|
194
197
|
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
198
|
+
this.usageDeduper.reset();
|
|
195
199
|
}
|
|
196
200
|
// No new content
|
|
197
201
|
if (currentSize <= this.filePosition) {
|
|
@@ -230,6 +234,7 @@ class ClaudeCodeReader {
|
|
|
230
234
|
this.committedPosition = 0;
|
|
231
235
|
this.parser.reset();
|
|
232
236
|
this.decoder = new string_decoder_1.StringDecoder('utf8');
|
|
237
|
+
this.usageDeduper.reset();
|
|
233
238
|
this._wasTruncated = false;
|
|
234
239
|
}
|
|
235
240
|
exists() {
|
|
@@ -255,44 +260,6 @@ class ClaudeCodeReader {
|
|
|
255
260
|
return this._wasTruncated;
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
|
-
function normalizeClaudeUsage(event) {
|
|
259
|
-
const message = event.message;
|
|
260
|
-
const usage = message?.usage;
|
|
261
|
-
if (!message || !usage) {
|
|
262
|
-
return {
|
|
263
|
-
...event,
|
|
264
|
-
providerMetadata: {
|
|
265
|
-
...event.providerMetadata,
|
|
266
|
-
providerId: 'claude-code',
|
|
267
|
-
source: 'claude-code-jsonl',
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
return {
|
|
272
|
-
...event,
|
|
273
|
-
message: {
|
|
274
|
-
...message,
|
|
275
|
-
normalizedUsage: (0, usageNormalization_1.normalizeProviderUsage)({
|
|
276
|
-
semantics: 'anthropic',
|
|
277
|
-
provider: 'anthropic',
|
|
278
|
-
source: 'claude-code-jsonl',
|
|
279
|
-
model: message.model,
|
|
280
|
-
inputTokens: usage.input_tokens,
|
|
281
|
-
outputTokens: usage.output_tokens,
|
|
282
|
-
cacheReadTokens: usage.cache_read_input_tokens,
|
|
283
|
-
cacheWriteTokens: usage.cache_creation_input_tokens,
|
|
284
|
-
reasoningTokens: usage.reasoning_tokens,
|
|
285
|
-
reasoningIncludedInOutput: false,
|
|
286
|
-
reportedCostUsd: usage.reported_cost,
|
|
287
|
-
}),
|
|
288
|
-
},
|
|
289
|
-
providerMetadata: {
|
|
290
|
-
...event.providerMetadata,
|
|
291
|
-
providerId: 'claude-code',
|
|
292
|
-
source: 'claude-code-jsonl',
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
263
|
/**
|
|
297
264
|
* Session provider for Claude Code CLI.
|
|
298
265
|
*
|
package/dist/providers/codex.js
CHANGED
|
@@ -52,6 +52,7 @@ const sessionContext_1 = require("../context/sessionContext");
|
|
|
52
52
|
const sessionStats_1 = require("../sessionStats");
|
|
53
53
|
const codexParser_1 = require("../parsers/codexParser");
|
|
54
54
|
const codexDatabase_1 = require("./codexDatabase");
|
|
55
|
+
const subagentStatsCache_1 = require("../parsers/subagentStatsCache");
|
|
55
56
|
const codexProfiles_1 = require("../codexProfiles");
|
|
56
57
|
const rolloutWalker_1 = require("./rolloutWalker");
|
|
57
58
|
const directoryListingCache_1 = require("./directoryListingCache");
|
|
@@ -59,6 +60,7 @@ const diagnostics_1 = require("./diagnostics");
|
|
|
59
60
|
const modelContext_1 = require("../modelContext");
|
|
60
61
|
const observedContextWindows_1 = require("../observedContextWindows");
|
|
61
62
|
const usageNormalization_1 = require("../usageNormalization");
|
|
63
|
+
const subagentUsage_1 = require("../usage/subagentUsage");
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
// Helper functions
|
|
64
66
|
// ---------------------------------------------------------------------------
|
|
@@ -1048,16 +1050,24 @@ class CodexProvider {
|
|
|
1048
1050
|
// Codex tracks forked sessions via forked_from_id in session_meta.
|
|
1049
1051
|
// Use DB when available, fall back to filesystem scanning.
|
|
1050
1052
|
const subagents = [];
|
|
1051
|
-
// DB approach:
|
|
1053
|
+
// DB approach: spawned subagents (parent recorded in `threads.source`),
|
|
1054
|
+
// plus forked children on older schemas that carry `forked_from_id`.
|
|
1052
1055
|
const db = this.ensureDb();
|
|
1053
1056
|
if (db) {
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1057
|
+
const seen = new Set();
|
|
1058
|
+
const children = [
|
|
1059
|
+
...db.getSpawnedChildThreads(sessionId),
|
|
1060
|
+
...db.getThreadsByForkedFromId(sessionId),
|
|
1061
|
+
].filter((thread) => !seen.has(thread.id) && seen.add(thread.id));
|
|
1062
|
+
for (const thread of children) {
|
|
1063
|
+
const built = (0, subagentStatsCache_1.cachedSubagentStats)(thread.rollout_path, () => this.buildSubagentStats(thread.id, thread.rollout_path, thread.title || thread.agent_nickname));
|
|
1064
|
+
if (built) {
|
|
1065
|
+
subagents.push(thread.agent_role && !built.agentType
|
|
1066
|
+
? { ...built, agentType: thread.agent_role }
|
|
1067
|
+
: built);
|
|
1068
|
+
}
|
|
1059
1069
|
}
|
|
1060
|
-
if (
|
|
1070
|
+
if (children.length > 0)
|
|
1061
1071
|
return subagents;
|
|
1062
1072
|
}
|
|
1063
1073
|
// Filesystem fallback: scan recent rollout files for a matching
|
|
@@ -1065,9 +1075,13 @@ class CodexProvider {
|
|
|
1065
1075
|
// of enumerating the whole history and slicing afterwards.
|
|
1066
1076
|
for (const file of findRolloutFilesInConfiguredHomes({ limit: 50 })) {
|
|
1067
1077
|
const meta = readSessionMeta(file.path);
|
|
1068
|
-
if (meta
|
|
1078
|
+
if (meta &&
|
|
1079
|
+
(meta.forked_from_id === sessionId || (0, codexDatabase_1.spawnParentFromSource)(meta.source) === sessionId)) {
|
|
1069
1080
|
const childId = (0, rolloutWalker_1.extractRolloutSessionId)(path.basename(file.path));
|
|
1070
|
-
|
|
1081
|
+
// `source` is a plain string for top-level sessions but an object
|
|
1082
|
+
// (`{ subagent: { thread_spawn: … } }`) for spawned children.
|
|
1083
|
+
const description = typeof meta.source === 'string' ? meta.source : undefined;
|
|
1084
|
+
const stats = (0, subagentStatsCache_1.cachedSubagentStats)(file.path, () => this.buildSubagentStats(childId, file.path, description));
|
|
1071
1085
|
if (stats)
|
|
1072
1086
|
subagents.push(stats);
|
|
1073
1087
|
}
|
|
@@ -1119,11 +1133,11 @@ class CodexProvider {
|
|
|
1119
1133
|
}
|
|
1120
1134
|
}
|
|
1121
1135
|
}
|
|
1122
|
-
// Accumulate tokens from usage events
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1136
|
+
// Accumulate tokens from usage events (duplicate token_count events
|
|
1137
|
+
// are already dropped by the parser).
|
|
1138
|
+
const usage = (0, usageNormalization_1.extractNormalizedUsage)(event);
|
|
1139
|
+
if (usage)
|
|
1140
|
+
(0, subagentUsage_1.addUsageToSubagent)(stats, usage);
|
|
1127
1141
|
}
|
|
1128
1142
|
}
|
|
1129
1143
|
catch {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { CodexDbThread } from '../types/codex';
|
|
6
6
|
import type { ProviderRuntimeStatus } from './types';
|
|
7
|
+
/** Parent thread id recorded in a Codex `threads.source` JSON value, if any. */
|
|
8
|
+
export declare function spawnParentFromSource(source: unknown): string | undefined;
|
|
7
9
|
export declare class CodexDatabase {
|
|
8
10
|
private readonly dbPath;
|
|
9
11
|
private sqlite3Available;
|
|
@@ -29,8 +31,21 @@ export declare class CodexDatabase {
|
|
|
29
31
|
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
30
32
|
*/
|
|
31
33
|
getThreadsByIdsAsync(ids: readonly string[]): Promise<CodexDbThread[]>;
|
|
32
|
-
|
|
34
|
+
private threadColumns;
|
|
35
|
+
/** Column names of the `threads` table (cached; the schema varies across Codex versions). */
|
|
36
|
+
private getThreadColumns;
|
|
37
|
+
/**
|
|
38
|
+
* Get all threads forked from a given session ID. Older Codex schemas carry
|
|
39
|
+
* a `forked_from_id` column; newer ones drop it, so the query only runs
|
|
40
|
+
* when the column exists (otherwise it fails and reports a diagnostic).
|
|
41
|
+
*/
|
|
33
42
|
getThreadsByForkedFromId(parentId: string): CodexDbThread[];
|
|
43
|
+
/**
|
|
44
|
+
* Get the subagent threads a session spawned. Codex records the parent in
|
|
45
|
+
* the `source` JSON (`{"subagent":{"thread_spawn":{"parent_thread_id":…}}}`);
|
|
46
|
+
* the LIKE narrows the scan and the JSON parse confirms the match.
|
|
47
|
+
*/
|
|
48
|
+
getSpawnedChildThreads(parentId: string): CodexDbThread[];
|
|
34
49
|
/** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
|
|
35
50
|
getDbMtime(): number;
|
|
36
51
|
}
|
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.CodexDatabase = void 0;
|
|
41
|
+
exports.spawnParentFromSource = spawnParentFromSource;
|
|
41
42
|
const fs = __importStar(require("fs"));
|
|
42
43
|
const path = __importStar(require("path"));
|
|
43
44
|
const child_process_1 = require("child_process");
|
|
@@ -46,6 +47,25 @@ const child_process_1 = require("child_process");
|
|
|
46
47
|
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
47
48
|
*/
|
|
48
49
|
const SQL_ID_CHUNK_SIZE = 400;
|
|
50
|
+
/** Parent thread id recorded in a Codex `threads.source` JSON value, if any. */
|
|
51
|
+
function spawnParentFromSource(source) {
|
|
52
|
+
let parsed = source;
|
|
53
|
+
if (typeof source === 'string') {
|
|
54
|
+
if (!source.startsWith('{'))
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(source);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!parsed || typeof parsed !== 'object')
|
|
64
|
+
return undefined;
|
|
65
|
+
const parent = parsed
|
|
66
|
+
.subagent?.thread_spawn?.parent_thread_id;
|
|
67
|
+
return typeof parent === 'string' ? parent : undefined;
|
|
68
|
+
}
|
|
49
69
|
class CodexDatabase {
|
|
50
70
|
dbPath;
|
|
51
71
|
sqlite3Available = null;
|
|
@@ -196,10 +216,38 @@ class CodexDatabase {
|
|
|
196
216
|
}
|
|
197
217
|
return results;
|
|
198
218
|
}
|
|
199
|
-
|
|
219
|
+
threadColumns = null;
|
|
220
|
+
/** Column names of the `threads` table (cached; the schema varies across Codex versions). */
|
|
221
|
+
getThreadColumns() {
|
|
222
|
+
if (!this.threadColumns) {
|
|
223
|
+
const rows = this.query("SELECT name FROM pragma_table_info('threads')");
|
|
224
|
+
if (rows.length === 0)
|
|
225
|
+
return new Set();
|
|
226
|
+
this.threadColumns = new Set(rows.map((row) => row.name));
|
|
227
|
+
}
|
|
228
|
+
return this.threadColumns;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get all threads forked from a given session ID. Older Codex schemas carry
|
|
232
|
+
* a `forked_from_id` column; newer ones drop it, so the query only runs
|
|
233
|
+
* when the column exists (otherwise it fails and reports a diagnostic).
|
|
234
|
+
*/
|
|
200
235
|
getThreadsByForkedFromId(parentId) {
|
|
236
|
+
if (!this.getThreadColumns().has('forked_from_id'))
|
|
237
|
+
return [];
|
|
201
238
|
return this.query('SELECT * FROM threads WHERE forked_from_id = ? ORDER BY created_at ASC', [parentId]);
|
|
202
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Get the subagent threads a session spawned. Codex records the parent in
|
|
242
|
+
* the `source` JSON (`{"subagent":{"thread_spawn":{"parent_thread_id":…}}}`);
|
|
243
|
+
* the LIKE narrows the scan and the JSON parse confirms the match.
|
|
244
|
+
*/
|
|
245
|
+
getSpawnedChildThreads(parentId) {
|
|
246
|
+
if (!this.getThreadColumns().has('source'))
|
|
247
|
+
return [];
|
|
248
|
+
const rows = this.query('SELECT * FROM threads WHERE source LIKE ? ORDER BY created_at ASC', [`%${parentId}%`]);
|
|
249
|
+
return rows.filter((row) => spawnParentFromSource(row.source) === parentId);
|
|
250
|
+
}
|
|
203
251
|
/** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
|
|
204
252
|
getDbMtime() {
|
|
205
253
|
try {
|
|
@@ -60,6 +60,8 @@ const sessionContext_1 = require("../context/sessionContext");
|
|
|
60
60
|
const sessionStats_1 = require("../sessionStats");
|
|
61
61
|
const openCodeParser_1 = require("../parsers/openCodeParser");
|
|
62
62
|
const openCodeDatabase_1 = require("./openCodeDatabase");
|
|
63
|
+
const usageNormalization_1 = require("../usageNormalization");
|
|
64
|
+
const subagentUsage_1 = require("../usage/subagentUsage");
|
|
63
65
|
const zaiQuota_1 = require("../zaiQuota");
|
|
64
66
|
const diagnostics_1 = require("./diagnostics");
|
|
65
67
|
const modelContext_1 = require("../modelContext");
|
|
@@ -1478,21 +1480,41 @@ class OpenCodeProvider {
|
|
|
1478
1480
|
const subtask = i < subtaskParts.length ? subtaskParts[i] : undefined;
|
|
1479
1481
|
// Aggregate tokens from child session messages
|
|
1480
1482
|
const childMessages = db.getMessagesForSession(child.id);
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
+
const tokenTotals = {
|
|
1484
|
+
agentId: '',
|
|
1485
|
+
toolCalls: [],
|
|
1486
|
+
inputTokens: 0,
|
|
1487
|
+
outputTokens: 0,
|
|
1488
|
+
};
|
|
1483
1489
|
for (const msgRow of childMessages) {
|
|
1484
1490
|
try {
|
|
1485
1491
|
const msgData = JSON.parse(msgRow.data);
|
|
1486
1492
|
const tokens = msgData.tokens;
|
|
1487
1493
|
if (tokens) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1494
|
+
// Same semantics as the main-session parser (openCodeParser).
|
|
1495
|
+
const cache = tokens.cache;
|
|
1496
|
+
const providerID = msgData.providerID;
|
|
1497
|
+
(0, subagentUsage_1.addUsageToSubagent)(tokenTotals, (0, usageNormalization_1.normalizeProviderUsage)({
|
|
1498
|
+
semantics: providerID === 'openai'
|
|
1499
|
+
? 'openai'
|
|
1500
|
+
: providerID === 'anthropic'
|
|
1501
|
+
? 'anthropic'
|
|
1502
|
+
: 'sidekick',
|
|
1503
|
+
provider: providerID ?? 'opencode',
|
|
1504
|
+
inputTokens: tokens.input,
|
|
1505
|
+
outputTokens: tokens.output,
|
|
1506
|
+
cacheReadTokens: cache?.read,
|
|
1507
|
+
cacheWriteTokens: cache?.write,
|
|
1508
|
+
reasoningTokens: tokens.reasoning,
|
|
1509
|
+
reasoningIncludedInOutput: providerID === 'openai',
|
|
1510
|
+
}));
|
|
1490
1511
|
}
|
|
1491
1512
|
}
|
|
1492
1513
|
catch {
|
|
1493
1514
|
// Skip malformed messages
|
|
1494
1515
|
}
|
|
1495
1516
|
}
|
|
1517
|
+
const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, totalTokens, } = tokenTotals;
|
|
1496
1518
|
// Extract tool calls from child session parts
|
|
1497
1519
|
const childParts = db.getPartsForSession(child.id);
|
|
1498
1520
|
const toolCalls = [];
|
|
@@ -1547,6 +1569,10 @@ class OpenCodeProvider {
|
|
|
1547
1569
|
toolCalls,
|
|
1548
1570
|
inputTokens,
|
|
1549
1571
|
outputTokens,
|
|
1572
|
+
cacheReadTokens,
|
|
1573
|
+
cacheWriteTokens,
|
|
1574
|
+
reasoningTokens,
|
|
1575
|
+
totalTokens,
|
|
1550
1576
|
startTime,
|
|
1551
1577
|
endTime,
|
|
1552
1578
|
durationMs,
|
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.generateHtmlReport = generateHtmlReport;
|
|
9
9
|
const costProvenance_1 = require("../aggregation/costProvenance");
|
|
10
10
|
const tokenSummary_1 = require("../tokenSummary");
|
|
11
|
+
const sessionTokenTotals_1 = require("../sessionTokenTotals");
|
|
11
12
|
const htmlHelpers_1 = require("./htmlHelpers");
|
|
12
13
|
const logo_1 = require("./logo");
|
|
13
14
|
/**
|
|
@@ -16,7 +17,7 @@ const logo_1 = require("./logo");
|
|
|
16
17
|
function generateHtmlReport(metrics, transcript, options = {}) {
|
|
17
18
|
const { sessionFileName, includeThinking = true, includeToolDetail = true, theme = 'dark', } = options;
|
|
18
19
|
const duration = (0, htmlHelpers_1.formatDuration)(metrics.sessionStartTime, metrics.lastEventTime);
|
|
19
|
-
const
|
|
20
|
+
const tokens = (0, sessionTokenTotals_1.combineSessionTokenTotals)(metrics.tokens, options.subagents ?? []);
|
|
20
21
|
return `<!DOCTYPE html>
|
|
21
22
|
<html lang="en" data-theme="${theme}">
|
|
22
23
|
<head>
|
|
@@ -27,7 +28,7 @@ ${generateStyles()}
|
|
|
27
28
|
</head>
|
|
28
29
|
<body>
|
|
29
30
|
${generateHeader(metrics, sessionFileName, duration)}
|
|
30
|
-
${generateStatsCards(metrics,
|
|
31
|
+
${generateStatsCards(metrics, tokens, duration)}
|
|
31
32
|
${generateModelBreakdown(metrics)}
|
|
32
33
|
${generateToolBreakdown(metrics)}
|
|
33
34
|
${generateTranscriptControls()}
|
|
@@ -422,7 +423,9 @@ function generateHeader(metrics, sessionFileName, duration) {
|
|
|
422
423
|
</div>
|
|
423
424
|
</div>`;
|
|
424
425
|
}
|
|
425
|
-
function generateStatsCards(metrics,
|
|
426
|
+
function generateStatsCards(metrics, tokens, duration) {
|
|
427
|
+
const main = tokens.mainThread;
|
|
428
|
+
const hasSubagents = tokens.subagents.length > 0;
|
|
426
429
|
const toolCount = metrics.toolStats.reduce((sum, t) => sum + t.successCount + t.failureCount, 0);
|
|
427
430
|
const cards = [
|
|
428
431
|
{ label: 'Messages', value: String(metrics.messageCount), sub: `${metrics.eventCount} events` },
|
|
@@ -432,14 +435,28 @@ function generateStatsCards(metrics, totalTokens, duration) {
|
|
|
432
435
|
sub: metrics.sessionStartTime ? (0, htmlHelpers_1.formatTimestamp)(metrics.sessionStartTime) + ' start' : '',
|
|
433
436
|
},
|
|
434
437
|
{
|
|
435
|
-
label: tokenSummary_1.TOKEN_TOTAL_LABEL,
|
|
436
|
-
value: (0, htmlHelpers_1.fmtTokens)(
|
|
437
|
-
sub: `${(0, htmlHelpers_1.fmtTokens)(
|
|
438
|
+
label: hasSubagents ? `${tokenSummary_1.TOKEN_MAIN_THREAD_LABEL} — ${tokenSummary_1.TOKEN_TOTAL_LABEL}` : tokenSummary_1.TOKEN_TOTAL_LABEL,
|
|
439
|
+
value: (0, htmlHelpers_1.fmtTokens)(main.total),
|
|
440
|
+
sub: `${(0, htmlHelpers_1.fmtTokens)(main.input)} in · ${(0, htmlHelpers_1.fmtTokens)(main.cacheRead)} cache read · ${(0, htmlHelpers_1.fmtTokens)(main.cacheWrite)} cache write · ${(0, htmlHelpers_1.fmtTokens)(main.output)} out${main.billedOutsideBuckets > 0 ? ` · ${(0, htmlHelpers_1.fmtTokens)(main.billedOutsideBuckets)} reasoning` : ''}`,
|
|
438
441
|
},
|
|
442
|
+
...(hasSubagents
|
|
443
|
+
? [
|
|
444
|
+
{
|
|
445
|
+
label: `${tokenSummary_1.TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length})`,
|
|
446
|
+
value: (0, htmlHelpers_1.fmtTokens)(tokens.subagentTotal.total),
|
|
447
|
+
sub: `${(0, htmlHelpers_1.fmtTokens)(tokens.subagentTotal.cacheRead)} cache read · ${(0, htmlHelpers_1.fmtTokens)(tokens.subagentTotal.output)} out`,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
label: tokenSummary_1.TOKEN_SESSION_TOTAL_LABEL,
|
|
451
|
+
value: (0, htmlHelpers_1.fmtTokens)(tokens.combined.total),
|
|
452
|
+
sub: 'main thread + subagents',
|
|
453
|
+
},
|
|
454
|
+
]
|
|
455
|
+
: []),
|
|
439
456
|
{
|
|
440
|
-
label: 'Cache',
|
|
441
|
-
value:
|
|
442
|
-
sub: `${(0, htmlHelpers_1.fmtTokens)(
|
|
457
|
+
label: 'Cache hit',
|
|
458
|
+
value: main.cacheHitRatio === null ? '—' : `${Math.round(main.cacheHitRatio * 100)}%`,
|
|
459
|
+
sub: `${(0, htmlHelpers_1.fmtTokens)(main.cacheRead)} of ${(0, htmlHelpers_1.fmtTokens)(main.context)} context tokens`,
|
|
443
460
|
},
|
|
444
461
|
{
|
|
445
462
|
label: 'Tool Calls',
|
|
@@ -474,7 +491,7 @@ function generateModelBreakdown(metrics) {
|
|
|
474
491
|
return `<div class="section">
|
|
475
492
|
<div class="section-title">Model Breakdown</div>
|
|
476
493
|
<table>
|
|
477
|
-
<thead><tr><th>Model</th><th>Calls</th><th
|
|
494
|
+
<thead><tr><th>Model</th><th>Calls</th><th>${tokenSummary_1.TOKEN_TOTAL_LABEL}</th><th>In / Out</th><th>Cost</th></tr></thead>
|
|
478
495
|
<tbody>${rows}</tbody>
|
|
479
496
|
</table>
|
|
480
497
|
</div>`;
|
|
@@ -534,7 +551,12 @@ function renderMessage(entry, includeThinking, includeToolDetail) {
|
|
|
534
551
|
? `<span class="message-model">${(0, htmlHelpers_1.escapeHtml)(entry.model)}</span>`
|
|
535
552
|
: '';
|
|
536
553
|
const tokenStr = entry.usage
|
|
537
|
-
? `<span class="message-tokens">${(0, htmlHelpers_1.fmtTokens)(
|
|
554
|
+
? `<span class="message-tokens">${(0, htmlHelpers_1.fmtTokens)((0, tokenSummary_1.summarizeTokens)({
|
|
555
|
+
inputTokens: entry.usage.input_tokens,
|
|
556
|
+
outputTokens: entry.usage.output_tokens,
|
|
557
|
+
cacheWriteTokens: entry.usage.cache_creation_input_tokens,
|
|
558
|
+
cacheReadTokens: entry.usage.cache_read_input_tokens,
|
|
559
|
+
}).total)} tokens incl. cache</span>`
|
|
538
560
|
: '';
|
|
539
561
|
const bodyParts = [];
|
|
540
562
|
for (const block of entry.content) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { AggregatedMetrics } from '../aggregation/types';
|
|
12
12
|
import type { SessionProviderBase } from '../providers/types';
|
|
13
|
-
import type { SessionEvent } from '../types/sessionEvent';
|
|
13
|
+
import type { SessionEvent, SubagentStats } from '../types/sessionEvent';
|
|
14
14
|
import type { TranscriptEntry } from './types';
|
|
15
15
|
export interface SessionReportInputs {
|
|
16
16
|
sessionPath: string;
|
|
@@ -18,6 +18,8 @@ export interface SessionReportInputs {
|
|
|
18
18
|
events: SessionEvent[];
|
|
19
19
|
metrics: AggregatedMetrics;
|
|
20
20
|
transcript: TranscriptEntry[];
|
|
21
|
+
/** The session's subagents; pass as `HtmlReportOptions.subagents` for the session total. */
|
|
22
|
+
subagents: SubagentStats[];
|
|
21
23
|
}
|
|
22
24
|
/** Read a session once and build the aggregator metrics and report transcript from it. */
|
|
23
25
|
export declare function readSessionReportInputs(provider: SessionProviderBase, sessionPath: string): SessionReportInputs;
|
|
@@ -13,6 +13,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.readSessionReportInputs = readSessionReportInputs;
|
|
14
14
|
const EventAggregator_1 = require("../aggregation/EventAggregator");
|
|
15
15
|
const sessionStats_1 = require("../sessionStats");
|
|
16
|
+
const sessionTokenTotals_1 = require("../sessionTokenTotals");
|
|
16
17
|
const transcriptParser_1 = require("./transcriptParser");
|
|
17
18
|
/** Read a session once and build the aggregator metrics and report transcript from it. */
|
|
18
19
|
function readSessionReportInputs(provider, sessionPath) {
|
|
@@ -30,5 +31,6 @@ function readSessionReportInputs(provider, sessionPath) {
|
|
|
30
31
|
events,
|
|
31
32
|
metrics: aggregator.getMetrics(),
|
|
32
33
|
transcript: (0, transcriptParser_1.parseTranscriptFromEvents)(events),
|
|
34
|
+
subagents: (0, sessionTokenTotals_1.scanSessionSubagents)(provider, sessionPath),
|
|
33
35
|
};
|
|
34
36
|
}
|
package/dist/report/types.d.ts
CHANGED
|
@@ -42,4 +42,9 @@ export interface HtmlReportOptions {
|
|
|
42
42
|
includeToolDetail?: boolean;
|
|
43
43
|
/** Color theme (default: 'dark'). */
|
|
44
44
|
theme?: 'dark' | 'light';
|
|
45
|
+
/**
|
|
46
|
+
* The session's subagents (`provider.scanSubagents()`). Their tokens are
|
|
47
|
+
* reported beside the main thread and added to the session total.
|
|
48
|
+
*/
|
|
49
|
+
subagents?: readonly import('../types/sessionEvent').SubagentStats[];
|
|
45
50
|
}
|
|
@@ -51,8 +51,12 @@ export declare const sessionMessageSchema: z.ZodObject<{
|
|
|
51
51
|
source: z.ZodOptional<z.ZodString>;
|
|
52
52
|
}, z.core.$strip>;
|
|
53
53
|
}, z.core.$strip>>;
|
|
54
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
55
|
+
call: "call";
|
|
56
|
+
correction: "correction";
|
|
57
|
+
}>>;
|
|
54
58
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
55
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
59
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
56
60
|
}, z.core.$strip>;
|
|
57
61
|
export declare const permissionModeSchema: z.ZodEnum<{
|
|
58
62
|
default: "default";
|
|
@@ -141,8 +145,12 @@ export declare const userSessionEventSchema: z.ZodObject<{
|
|
|
141
145
|
source: z.ZodOptional<z.ZodString>;
|
|
142
146
|
}, z.core.$strip>;
|
|
143
147
|
}, z.core.$strip>>;
|
|
148
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
149
|
+
call: "call";
|
|
150
|
+
correction: "correction";
|
|
151
|
+
}>>;
|
|
144
152
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
145
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
153
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
146
154
|
}, z.core.$strip>;
|
|
147
155
|
}, z.core.$strip>;
|
|
148
156
|
export declare const assistantSessionEventSchema: z.ZodObject<{
|
|
@@ -226,8 +234,12 @@ export declare const assistantSessionEventSchema: z.ZodObject<{
|
|
|
226
234
|
source: z.ZodOptional<z.ZodString>;
|
|
227
235
|
}, z.core.$strip>;
|
|
228
236
|
}, z.core.$strip>>;
|
|
237
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
238
|
+
call: "call";
|
|
239
|
+
correction: "correction";
|
|
240
|
+
}>>;
|
|
229
241
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
230
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
242
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
231
243
|
}, z.core.$strip>;
|
|
232
244
|
}, z.core.$strip>;
|
|
233
245
|
export declare const summarySessionEventSchema: z.ZodType<SummarySessionEvent>;
|
|
@@ -312,8 +324,12 @@ export declare const systemSessionEventSchema: z.ZodObject<{
|
|
|
312
324
|
source: z.ZodOptional<z.ZodString>;
|
|
313
325
|
}, z.core.$strip>;
|
|
314
326
|
}, z.core.$strip>>;
|
|
327
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
328
|
+
call: "call";
|
|
329
|
+
correction: "correction";
|
|
330
|
+
}>>;
|
|
315
331
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
316
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
332
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
317
333
|
}, z.core.$strip>>;
|
|
318
334
|
}, z.core.$strip>;
|
|
319
335
|
export declare const toolUseSessionEventSchema: z.ZodObject<{
|
|
@@ -401,8 +417,12 @@ export declare const toolUseSessionEventSchema: z.ZodObject<{
|
|
|
401
417
|
source: z.ZodOptional<z.ZodString>;
|
|
402
418
|
}, z.core.$strip>;
|
|
403
419
|
}, z.core.$strip>>;
|
|
420
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
421
|
+
call: "call";
|
|
422
|
+
correction: "correction";
|
|
423
|
+
}>>;
|
|
404
424
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
405
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
425
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
406
426
|
}, z.core.$strip>>;
|
|
407
427
|
}, z.core.$strip>;
|
|
408
428
|
export declare const toolResultSessionEventSchema: z.ZodObject<{
|
|
@@ -491,8 +511,12 @@ export declare const toolResultSessionEventSchema: z.ZodObject<{
|
|
|
491
511
|
source: z.ZodOptional<z.ZodString>;
|
|
492
512
|
}, z.core.$strip>;
|
|
493
513
|
}, z.core.$strip>>;
|
|
514
|
+
usageKind: z.ZodOptional<z.ZodEnum<{
|
|
515
|
+
call: "call";
|
|
516
|
+
correction: "correction";
|
|
517
|
+
}>>;
|
|
494
518
|
content: z.ZodOptional<z.ZodUnknown>;
|
|
495
|
-
stop_reason: z.ZodOptional<z.ZodString
|
|
519
|
+
stop_reason: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
496
520
|
}, z.core.$strip>>;
|
|
497
521
|
}, z.core.$strip>;
|
|
498
522
|
export declare const sessionEventSchema: z.ZodType<SessionEvent>;
|
|
@@ -29,8 +29,14 @@ exports.sessionMessageSchema = zod_1.z.object({
|
|
|
29
29
|
model: zod_1.z.string().optional(),
|
|
30
30
|
usage: exports.messageUsageSchema.optional(),
|
|
31
31
|
normalizedUsage: usageNormalization_1.normalizedUsageSchema.optional(),
|
|
32
|
+
usageKind: zod_1.z.enum(['call', 'correction']).optional(),
|
|
32
33
|
content: zod_1.z.unknown().optional(),
|
|
33
|
-
stop_reason:
|
|
34
|
+
// Streaming lines can carry `stop_reason: null`; rejecting it dropped the
|
|
35
|
+
// whole event (text, tools, and usage), so normalize it to absent.
|
|
36
|
+
stop_reason: zod_1.z
|
|
37
|
+
.string()
|
|
38
|
+
.nullish()
|
|
39
|
+
.transform((value) => value ?? undefined),
|
|
34
40
|
});
|
|
35
41
|
// ── PermissionMode ──
|
|
36
42
|
exports.permissionModeSchema = zod_1.z.enum([
|