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.
Files changed (51) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +57 -14
  3. package/dist/aggregation/EventAggregator.d.ts +12 -2
  4. package/dist/aggregation/EventAggregator.js +49 -19
  5. package/dist/browser.d.ts +2 -1
  6. package/dist/browser.js +9 -2
  7. package/dist/extractors/tokenUsage.js +1 -0
  8. package/dist/formatters/sessionDump.d.ts +10 -2
  9. package/dist/formatters/sessionDump.js +45 -23
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.js +31 -10
  12. package/dist/node.d.ts +6 -1
  13. package/dist/node.js +12 -2
  14. package/dist/parsers/codexParser.d.ts +22 -2
  15. package/dist/parsers/codexParser.js +64 -5
  16. package/dist/parsers/subagentScanner.d.ts +9 -0
  17. package/dist/parsers/subagentScanner.js +45 -12
  18. package/dist/parsers/subagentStatsCache.d.ts +17 -0
  19. package/dist/parsers/subagentStatsCache.js +82 -0
  20. package/dist/parsers/subagentTraceParser.js +19 -10
  21. package/dist/providers/claudeCode.js +8 -41
  22. package/dist/providers/codex.js +28 -14
  23. package/dist/providers/codexDatabase.d.ts +16 -1
  24. package/dist/providers/codexDatabase.js +49 -1
  25. package/dist/providers/openCode.js +30 -4
  26. package/dist/report/htmlReportGenerator.js +33 -11
  27. package/dist/report/sessionReportInputs.d.ts +3 -1
  28. package/dist/report/sessionReportInputs.js +2 -0
  29. package/dist/report/types.d.ts +5 -0
  30. package/dist/schemas/sessionEvent.d.ts +30 -6
  31. package/dist/schemas/sessionEvent.js +7 -1
  32. package/dist/schemas/stateFile.d.ts +2 -0
  33. package/dist/schemas/stateFile.js +1 -0
  34. package/dist/sessionTokenTotals.d.ts +46 -0
  35. package/dist/sessionTokenTotals.js +138 -0
  36. package/dist/stateFile.d.ts +7 -0
  37. package/dist/tokenSummary.d.ts +27 -1
  38. package/dist/tokenSummary.js +50 -3
  39. package/dist/types/codex.d.ts +4 -0
  40. package/dist/types/sessionEvent.d.ts +22 -0
  41. package/dist/usage/claudeUsageDedupe.d.ts +53 -0
  42. package/dist/usage/claudeUsageDedupe.js +195 -0
  43. package/dist/usage/subagentUsage.d.ts +13 -0
  44. package/dist/usage/subagentUsage.js +21 -0
  45. package/dist/usage/usageEvents.d.ts +18 -2
  46. package/dist/usage/usageEvents.js +83 -8
  47. package/dist/watchers/eventBridge.js +5 -0
  48. package/dist/watchers/jsonlWatcher.d.ts +5 -0
  49. package/dist/watchers/jsonlWatcher.js +40 -5
  50. package/dist/watchers/types.d.ts +5 -0
  51. package/package.json +1 -1
@@ -42,16 +42,42 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.readAgentMeta = readAgentMeta;
45
46
  exports.scanSubagentDir = scanSubagentDir;
46
47
  exports.handleTaskUpdate = handleTaskUpdate;
47
48
  exports.extractTaskInfo = extractTaskInfo;
48
49
  const fs = __importStar(require("fs"));
49
50
  const path = __importStar(require("path"));
51
+ const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
52
+ const subagentUsage_1 = require("../usage/subagentUsage");
53
+ const subagentStatsCache_1 = require("./subagentStatsCache");
50
54
  /**
51
55
  * Pattern for matching subagent JSONL files.
52
56
  * Files are named like: agent-<hash>.jsonl
53
57
  */
54
58
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
59
+ /**
60
+ * Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
61
+ * for the spawn's agent type and description. Missing or malformed files
62
+ * yield an empty object.
63
+ */
64
+ function readAgentMeta(agentFilePath) {
65
+ try {
66
+ const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
67
+ const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
68
+ return {
69
+ ...(typeof parsed.agentType === 'string' && parsed.agentType
70
+ ? { agentType: parsed.agentType }
71
+ : {}),
72
+ ...(typeof parsed.description === 'string' && parsed.description
73
+ ? { description: parsed.description }
74
+ : {}),
75
+ };
76
+ }
77
+ catch {
78
+ return {};
79
+ }
80
+ }
55
81
  /** Task-related tool names */
56
82
  const TASK_TOOLS = ['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'];
57
83
  /**
@@ -118,7 +144,7 @@ function scanSubagentDir(sessionDir, sessionId, logger, skipFiles = new Set()) {
118
144
  }
119
145
  const agentId = match[1];
120
146
  const filePath = path.join(subagentsDir, file);
121
- const agentStats = parseAgentFile(filePath, agentId, log);
147
+ const agentStats = (0, subagentStatsCache_1.cachedSubagentStats)(filePath, () => parseAgentFile(filePath, agentId, log));
122
148
  if (agentStats) {
123
149
  log(`[SubagentScanner] Agent ${agentId}: ${agentStats.toolCalls.length} tool calls`);
124
150
  results.push(agentStats);
@@ -150,8 +176,8 @@ function parseAgentFile(filePath, agentId, log) {
150
176
  const toolCalls = [];
151
177
  let agentType;
152
178
  let description;
153
- let inputTokens = 0;
154
- let outputTokens = 0;
179
+ const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
180
+ const usageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
155
181
  let startTime;
156
182
  let endTime;
157
183
  // Task tracking state
@@ -168,12 +194,11 @@ function parseAgentFile(filePath, agentId, log) {
168
194
  if (!startTime)
169
195
  startTime = eventTimestamp;
170
196
  endTime = eventTimestamp;
171
- // Extract token usage from assistant messages
172
- if (event.type === 'assistant' && event.message?.usage) {
173
- const usage = event.message.usage;
174
- inputTokens += usage.input_tokens || 0;
175
- outputTokens += usage.output_tokens || 0;
176
- }
197
+ // Extract token usage from assistant messages. Split lines of one
198
+ // response repeat its usage, so count each response once.
199
+ const usage = (0, claudeUsageDedupe_1.dedupedRawClaudeUsage)(event, usageDeduper);
200
+ if (usage)
201
+ (0, subagentUsage_1.addUsageToSubagent)(tokens, usage);
177
202
  // Extract agent type and description from Task tool invocation
178
203
  // This appears in the parent session, but we can also look for it
179
204
  // in the first system message or context
@@ -269,8 +294,12 @@ function parseAgentFile(filePath, agentId, log) {
269
294
  // Skip malformed lines
270
295
  }
271
296
  }
297
+ // Claude Code writes the spawn's type and description beside the transcript.
298
+ const meta = readAgentMeta(filePath);
299
+ agentType = meta.agentType ?? agentType;
300
+ description = meta.description ?? description;
272
301
  // If we found any tool calls, return the stats
273
- if (toolCalls.length > 0 || agentType || description || inputTokens > 0) {
302
+ if (toolCalls.length > 0 || agentType || description || (tokens.totalTokens ?? 0) > 0) {
274
303
  const durationMs = startTime && endTime ? endTime.getTime() - startTime.getTime() : undefined;
275
304
  return {
276
305
  agentId,
@@ -278,8 +307,12 @@ function parseAgentFile(filePath, agentId, log) {
278
307
  description,
279
308
  toolCalls,
280
309
  taskState: taskState.tasks.size > 0 ? taskState : undefined,
281
- inputTokens,
282
- outputTokens,
310
+ inputTokens: tokens.inputTokens,
311
+ outputTokens: tokens.outputTokens,
312
+ cacheReadTokens: tokens.cacheReadTokens ?? 0,
313
+ cacheWriteTokens: tokens.cacheWriteTokens ?? 0,
314
+ reasoningTokens: tokens.reasoningTokens ?? 0,
315
+ totalTokens: tokens.totalTokens ?? 0,
283
316
  startTime,
284
317
  endTime,
285
318
  durationMs,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Memoizes per-transcript subagent stats by file size and mtime.
3
+ *
4
+ * Dashboards rescan a session's subagents on a timer, and one parent can own
5
+ * a hundred subagent transcripts of several MB each. An unchanged transcript
6
+ * now costs one `stat` instead of a full parse. Callers treat the returned
7
+ * stats as read-only.
8
+ *
9
+ * Node-only: stats files.
10
+ *
11
+ * @module parsers/subagentStatsCache
12
+ */
13
+ import type { SubagentStats } from '../types/sessionEvent';
14
+ /** Stats for one subagent transcript, parsed again only when the file changed. */
15
+ export declare function cachedSubagentStats(filePath: string, compute: () => SubagentStats | null): SubagentStats | null;
16
+ /** @internal Test hook. */
17
+ export declare function clearSubagentStatsCache(): void;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Memoizes per-transcript subagent stats by file size and mtime.
4
+ *
5
+ * Dashboards rescan a session's subagents on a timer, and one parent can own
6
+ * a hundred subagent transcripts of several MB each. An unchanged transcript
7
+ * now costs one `stat` instead of a full parse. Callers treat the returned
8
+ * stats as read-only.
9
+ *
10
+ * Node-only: stats files.
11
+ *
12
+ * @module parsers/subagentStatsCache
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.cachedSubagentStats = cachedSubagentStats;
49
+ exports.clearSubagentStatsCache = clearSubagentStatsCache;
50
+ const fs = __importStar(require("fs"));
51
+ const MAX_ENTRIES = 1024;
52
+ const cache = new Map();
53
+ /** Stats for one subagent transcript, parsed again only when the file changed. */
54
+ function cachedSubagentStats(filePath, compute) {
55
+ let stat;
56
+ try {
57
+ stat = fs.statSync(filePath);
58
+ }
59
+ catch {
60
+ cache.delete(filePath);
61
+ return compute();
62
+ }
63
+ const hit = cache.get(filePath);
64
+ if (hit && hit.size === stat.size && hit.mtimeMs === stat.mtimeMs) {
65
+ cache.delete(filePath);
66
+ cache.set(filePath, hit);
67
+ return hit.stats;
68
+ }
69
+ const stats = compute();
70
+ cache.delete(filePath);
71
+ cache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, stats });
72
+ if (cache.size > MAX_ENTRIES) {
73
+ const oldest = cache.keys().next().value;
74
+ if (oldest !== undefined)
75
+ cache.delete(oldest);
76
+ }
77
+ return stats;
78
+ }
79
+ /** @internal Test hook. */
80
+ function clearSubagentStatsCache() {
81
+ cache.clear();
82
+ }
@@ -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
- let inputTokens = 0;
105
- let outputTokens = 0;
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
- if (raw.type === 'assistant' && raw.message?.usage) {
157
- const usage = raw.message.usage;
158
- inputTokens += usage.input_tokens || 0;
159
- outputTokens += usage.output_tokens || 0;
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
- if (events.length === 0 && !agentType && !description && inputTokens === 0) {
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
- constructor(sessionPath) {
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
  *
@@ -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: query threads table for forked children
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 forkedThreads = db.getThreadsByForkedFromId(sessionId);
1055
- for (const thread of forkedThreads) {
1056
- const stats = this.buildSubagentStats(thread.id, thread.rollout_path, thread.title);
1057
- if (stats)
1058
- subagents.push(stats);
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 (forkedThreads.length > 0)
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?.forked_from_id === sessionId) {
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
- const stats = this.buildSubagentStats(childId, file.path, meta.source);
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
- if (event.message?.usage) {
1124
- stats.inputTokens += event.message.usage.input_tokens || 0;
1125
- stats.outputTokens += event.message.usage.output_tokens || 0;
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
- /** Get all threads forked from a given session ID. */
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
- /** Get all threads forked from a given session ID. */
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
- let inputTokens = 0;
1482
- let outputTokens = 0;
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
- inputTokens += tokens.input || 0;
1489
- outputTokens += tokens.output || 0;
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,