codex-devtools 0.1.11 → 0.2.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/dist-electron/main/chunks/{CodexServiceContext-CRkTP14W.cjs → CodexServiceContext-DaxLI918.cjs} +804 -71
- package/dist-electron/main/index.cjs +45 -2
- package/dist-electron/main/standalone.cjs +76 -15
- package/dist-electron/preload/index.cjs +2 -0
- package/out/renderer/assets/{index-BEzdp8iI.js → index-5ydAmpDO.js} +712 -26
- package/out/renderer/assets/{index-BTmVA30y.css → index-Cgat1ue6.css} +387 -16
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const fs = require("node:fs");
|
|
3
|
+
const node_crypto = require("node:crypto");
|
|
3
4
|
const path = require("node:path");
|
|
4
5
|
const node_buffer = require("node:buffer");
|
|
5
6
|
const os = require("node:os");
|
|
@@ -34,6 +35,12 @@ const createLogger = (scope) => {
|
|
|
34
35
|
debug: (message, ...args) => console.debug(prefix, message, ...args)
|
|
35
36
|
};
|
|
36
37
|
};
|
|
38
|
+
const DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH = path__namespace.join(
|
|
39
|
+
os__namespace.homedir(),
|
|
40
|
+
".config",
|
|
41
|
+
"codex-devtools",
|
|
42
|
+
"config.json"
|
|
43
|
+
);
|
|
37
44
|
const DEFAULT_CONFIG = {
|
|
38
45
|
general: {
|
|
39
46
|
launchAtLogin: false,
|
|
@@ -66,7 +73,7 @@ function deepMerge(target, source) {
|
|
|
66
73
|
return output;
|
|
67
74
|
}
|
|
68
75
|
class ConfigManager {
|
|
69
|
-
constructor(configPath =
|
|
76
|
+
constructor(configPath = DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH) {
|
|
70
77
|
this.configPath = configPath;
|
|
71
78
|
this.config = this.loadConfig();
|
|
72
79
|
}
|
|
@@ -117,68 +124,68 @@ class ConfigManager {
|
|
|
117
124
|
fs__namespace.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf8");
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
|
-
function isRecord(value) {
|
|
127
|
+
function isRecord$2(value) {
|
|
121
128
|
return typeof value === "object" && value !== null;
|
|
122
129
|
}
|
|
123
|
-
function isString(value) {
|
|
130
|
+
function isString$2(value) {
|
|
124
131
|
return typeof value === "string";
|
|
125
132
|
}
|
|
126
133
|
function hasString(obj, key) {
|
|
127
|
-
return isString(obj[key]);
|
|
134
|
+
return isString$2(obj[key]);
|
|
128
135
|
}
|
|
129
136
|
function hasOptionalString(obj, key) {
|
|
130
|
-
return !(key in obj) || isString(obj[key]);
|
|
137
|
+
return !(key in obj) || isString$2(obj[key]);
|
|
131
138
|
}
|
|
132
139
|
function hasOptionalRecord(obj, key) {
|
|
133
|
-
return !(key in obj) || isRecord(obj[key]);
|
|
140
|
+
return !(key in obj) || isRecord$2(obj[key]);
|
|
134
141
|
}
|
|
135
142
|
function getContentBlockText(block) {
|
|
136
143
|
if (!("text" in block)) {
|
|
137
144
|
return "";
|
|
138
145
|
}
|
|
139
|
-
return isString(block.text) ? block.text : "";
|
|
146
|
+
return isString$2(block.text) ? block.text : "";
|
|
140
147
|
}
|
|
141
148
|
function isReasoningSummaryText(value) {
|
|
142
|
-
return isRecord(value) && hasString(value, "text");
|
|
149
|
+
return isRecord$2(value) && hasString(value, "text");
|
|
143
150
|
}
|
|
144
151
|
function reasoningSummaryItemToText(item) {
|
|
145
|
-
return isString(item) ? item : item.text;
|
|
152
|
+
return isString$2(item) ? item : item.text;
|
|
146
153
|
}
|
|
147
154
|
function reasoningSummaryToText(summary) {
|
|
148
155
|
return summary.map(reasoningSummaryItemToText);
|
|
149
156
|
}
|
|
150
157
|
function isContentBlockInputText(value) {
|
|
151
|
-
if (!isRecord(value)) {
|
|
158
|
+
if (!isRecord$2(value)) {
|
|
152
159
|
return false;
|
|
153
160
|
}
|
|
154
161
|
return value.type === "input_text" && hasString(value, "text");
|
|
155
162
|
}
|
|
156
163
|
function isContentBlockOutputText(value) {
|
|
157
|
-
if (!isRecord(value)) {
|
|
164
|
+
if (!isRecord$2(value)) {
|
|
158
165
|
return false;
|
|
159
166
|
}
|
|
160
167
|
return value.type === "output_text" && hasString(value, "text");
|
|
161
168
|
}
|
|
162
169
|
function isContentBlockInputImage(value) {
|
|
163
|
-
if (!isRecord(value)) {
|
|
170
|
+
if (!isRecord$2(value)) {
|
|
164
171
|
return false;
|
|
165
172
|
}
|
|
166
173
|
return value.type === "input_image" && hasString(value, "image_url");
|
|
167
174
|
}
|
|
168
175
|
function isContentBlockUnknown(value) {
|
|
169
|
-
if (!isRecord(value) || !hasString(value, "type")) {
|
|
176
|
+
if (!isRecord$2(value) || !hasString(value, "type")) {
|
|
170
177
|
return false;
|
|
171
178
|
}
|
|
172
179
|
if (value.type === "input_text" || value.type === "output_text" || value.type === "input_image") {
|
|
173
180
|
return false;
|
|
174
181
|
}
|
|
175
|
-
return !("text" in value) || isString(value.text);
|
|
182
|
+
return !("text" in value) || isString$2(value.text);
|
|
176
183
|
}
|
|
177
184
|
function isContentBlock(value) {
|
|
178
185
|
return isContentBlockInputText(value) || isContentBlockOutputText(value) || isContentBlockInputImage(value) || isContentBlockUnknown(value);
|
|
179
186
|
}
|
|
180
187
|
function isMessagePayload(value) {
|
|
181
|
-
if (!isRecord(value)) {
|
|
188
|
+
if (!isRecord$2(value)) {
|
|
182
189
|
return false;
|
|
183
190
|
}
|
|
184
191
|
if (value.type !== "message" || value.role !== "developer" && value.role !== "user" && value.role !== "assistant") {
|
|
@@ -190,70 +197,70 @@ function isMessagePayload(value) {
|
|
|
190
197
|
return value.content.every(isContentBlock);
|
|
191
198
|
}
|
|
192
199
|
function isFunctionCallPayload(value) {
|
|
193
|
-
if (!isRecord(value)) {
|
|
200
|
+
if (!isRecord$2(value)) {
|
|
194
201
|
return false;
|
|
195
202
|
}
|
|
196
203
|
return value.type === "function_call" && hasString(value, "name") && hasString(value, "arguments") && hasString(value, "call_id");
|
|
197
204
|
}
|
|
198
205
|
function isFunctionCallOutputPayload(value) {
|
|
199
|
-
if (!isRecord(value)) {
|
|
206
|
+
if (!isRecord$2(value)) {
|
|
200
207
|
return false;
|
|
201
208
|
}
|
|
202
209
|
return value.type === "function_call_output" && hasString(value, "call_id") && hasString(value, "output");
|
|
203
210
|
}
|
|
204
211
|
function isReasoningPayload(value) {
|
|
205
|
-
if (!isRecord(value)) {
|
|
212
|
+
if (!isRecord$2(value)) {
|
|
206
213
|
return false;
|
|
207
214
|
}
|
|
208
215
|
if (value.type !== "reasoning" || !Array.isArray(value.summary)) {
|
|
209
216
|
return false;
|
|
210
217
|
}
|
|
211
|
-
if (!value.summary.every((item) => isString(item) || isReasoningSummaryText(item))) {
|
|
218
|
+
if (!value.summary.every((item) => isString$2(item) || isReasoningSummaryText(item))) {
|
|
212
219
|
return false;
|
|
213
220
|
}
|
|
214
|
-
return !("encrypted_content" in value) || isString(value.encrypted_content) || value.encrypted_content === null;
|
|
221
|
+
return !("encrypted_content" in value) || isString$2(value.encrypted_content) || value.encrypted_content === null;
|
|
215
222
|
}
|
|
216
223
|
function isResponseItemPayload(value) {
|
|
217
224
|
return isMessagePayload(value) || isFunctionCallPayload(value) || isFunctionCallOutputPayload(value) || isReasoningPayload(value);
|
|
218
225
|
}
|
|
219
226
|
function isTokenUsage(value) {
|
|
220
|
-
if (!isRecord(value)) {
|
|
227
|
+
if (!isRecord$2(value)) {
|
|
221
228
|
return false;
|
|
222
229
|
}
|
|
223
230
|
return typeof value.input_tokens === "number" && typeof value.cached_input_tokens === "number" && typeof value.output_tokens === "number" && typeof value.reasoning_output_tokens === "number" && typeof value.total_tokens === "number";
|
|
224
231
|
}
|
|
225
232
|
function isTokenCountPayload(value) {
|
|
226
|
-
if (!isRecord(value) || value.type !== "token_count") {
|
|
233
|
+
if (!isRecord$2(value) || value.type !== "token_count") {
|
|
227
234
|
return false;
|
|
228
235
|
}
|
|
229
236
|
if (value.info === null) {
|
|
230
237
|
return true;
|
|
231
238
|
}
|
|
232
|
-
if (!isRecord(value.info)) {
|
|
239
|
+
if (!isRecord$2(value.info)) {
|
|
233
240
|
return false;
|
|
234
241
|
}
|
|
235
242
|
return isTokenUsage(value.info.total_token_usage) && isTokenUsage(value.info.last_token_usage) && typeof value.info.model_context_window === "number";
|
|
236
243
|
}
|
|
237
244
|
function isAgentReasoningPayload(value) {
|
|
238
|
-
if (!isRecord(value)) {
|
|
245
|
+
if (!isRecord$2(value)) {
|
|
239
246
|
return false;
|
|
240
247
|
}
|
|
241
248
|
return value.type === "agent_reasoning" && hasString(value, "text");
|
|
242
249
|
}
|
|
243
250
|
function isAgentMessagePayload(value) {
|
|
244
|
-
if (!isRecord(value)) {
|
|
251
|
+
if (!isRecord$2(value)) {
|
|
245
252
|
return false;
|
|
246
253
|
}
|
|
247
254
|
return value.type === "agent_message" && hasString(value, "message");
|
|
248
255
|
}
|
|
249
256
|
function isUserMessagePayload(value) {
|
|
250
|
-
if (!isRecord(value)) {
|
|
257
|
+
if (!isRecord$2(value)) {
|
|
251
258
|
return false;
|
|
252
259
|
}
|
|
253
260
|
return value.type === "user_message" && hasString(value, "message");
|
|
254
261
|
}
|
|
255
262
|
function isContextCompactedPayload(value) {
|
|
256
|
-
if (!isRecord(value)) {
|
|
263
|
+
if (!isRecord$2(value)) {
|
|
257
264
|
return false;
|
|
258
265
|
}
|
|
259
266
|
return value.type === "context_compacted";
|
|
@@ -262,10 +269,10 @@ function isEventMsgPayload(value) {
|
|
|
262
269
|
return isTokenCountPayload(value) || isAgentReasoningPayload(value) || isAgentMessagePayload(value) || isUserMessagePayload(value) || isContextCompactedPayload(value);
|
|
263
270
|
}
|
|
264
271
|
function isSessionMetaEntry(value) {
|
|
265
|
-
if (!isRecord(value) || value.type !== "session_meta" || !hasString(value, "timestamp")) {
|
|
272
|
+
if (!isRecord$2(value) || value.type !== "session_meta" || !hasString(value, "timestamp")) {
|
|
266
273
|
return false;
|
|
267
274
|
}
|
|
268
|
-
if (!isRecord(value.payload)) {
|
|
275
|
+
if (!isRecord$2(value.payload)) {
|
|
269
276
|
return false;
|
|
270
277
|
}
|
|
271
278
|
if (!hasOptionalString(value.payload, "id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "originator") || !hasOptionalString(value.payload, "cli_version") || !hasOptionalString(value.payload, "model_provider") || !hasOptionalString(value.payload, "model")) {
|
|
@@ -273,14 +280,14 @@ function isSessionMetaEntry(value) {
|
|
|
273
280
|
}
|
|
274
281
|
if ("base_instructions" in value.payload) {
|
|
275
282
|
const baseInstructions = value.payload.base_instructions;
|
|
276
|
-
if (!isString(baseInstructions) && !isRecord(baseInstructions)) {
|
|
283
|
+
if (!isString$2(baseInstructions) && !isRecord$2(baseInstructions)) {
|
|
277
284
|
return false;
|
|
278
285
|
}
|
|
279
286
|
}
|
|
280
287
|
if (!hasOptionalRecord(value.payload, "git")) {
|
|
281
288
|
return false;
|
|
282
289
|
}
|
|
283
|
-
if (isRecord(value.payload.git)) {
|
|
290
|
+
if (isRecord$2(value.payload.git)) {
|
|
284
291
|
if (!hasOptionalString(value.payload.git, "commit_hash") || !hasOptionalString(value.payload.git, "branch") || !hasOptionalString(value.payload.git, "repository_url")) {
|
|
285
292
|
return false;
|
|
286
293
|
}
|
|
@@ -288,55 +295,55 @@ function isSessionMetaEntry(value) {
|
|
|
288
295
|
return hasString(value.payload, "id") || hasString(value.payload, "cwd");
|
|
289
296
|
}
|
|
290
297
|
function isResponseItemEntry(value) {
|
|
291
|
-
if (!isRecord(value) || value.type !== "response_item" || !hasString(value, "timestamp")) {
|
|
298
|
+
if (!isRecord$2(value) || value.type !== "response_item" || !hasString(value, "timestamp")) {
|
|
292
299
|
return false;
|
|
293
300
|
}
|
|
294
301
|
return isResponseItemPayload(value.payload);
|
|
295
302
|
}
|
|
296
303
|
function isTurnContextEntry(value) {
|
|
297
|
-
if (!isRecord(value) || value.type !== "turn_context" || !hasString(value, "timestamp")) {
|
|
304
|
+
if (!isRecord$2(value) || value.type !== "turn_context" || !hasString(value, "timestamp")) {
|
|
298
305
|
return false;
|
|
299
306
|
}
|
|
300
|
-
if (!isRecord(value.payload)) {
|
|
307
|
+
if (!isRecord$2(value.payload)) {
|
|
301
308
|
return false;
|
|
302
309
|
}
|
|
303
310
|
if (!hasOptionalString(value.payload, "turn_id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "approval_policy") || !hasOptionalString(value.payload, "model") || !hasOptionalString(value.payload, "personality") || !hasOptionalString(value.payload, "effort") || !hasOptionalString(value.payload, "summary") || !hasOptionalString(value.payload, "user_instructions")) {
|
|
304
311
|
return false;
|
|
305
312
|
}
|
|
306
313
|
const sandboxPolicy = value.payload.sandbox_policy;
|
|
307
|
-
if ("sandbox_policy" in value.payload && !(isString(sandboxPolicy) || isRecord(sandboxPolicy))) {
|
|
314
|
+
if ("sandbox_policy" in value.payload && !(isString$2(sandboxPolicy) || isRecord$2(sandboxPolicy))) {
|
|
308
315
|
return false;
|
|
309
316
|
}
|
|
310
317
|
const collaborationMode = value.payload.collaboration_mode;
|
|
311
|
-
if ("collaboration_mode" in value.payload && !(isString(collaborationMode) || isRecord(collaborationMode))) {
|
|
318
|
+
if ("collaboration_mode" in value.payload && !(isString$2(collaborationMode) || isRecord$2(collaborationMode))) {
|
|
312
319
|
return false;
|
|
313
320
|
}
|
|
314
321
|
const truncationPolicy = value.payload.truncation_policy;
|
|
315
|
-
if ("truncation_policy" in value.payload && !(isString(truncationPolicy) || isRecord(truncationPolicy))) {
|
|
322
|
+
if ("truncation_policy" in value.payload && !(isString$2(truncationPolicy) || isRecord$2(truncationPolicy))) {
|
|
316
323
|
return false;
|
|
317
324
|
}
|
|
318
325
|
return hasString(value.payload, "cwd") || hasString(value.payload, "model");
|
|
319
326
|
}
|
|
320
327
|
function isEventMsgEntry(value) {
|
|
321
|
-
if (!isRecord(value) || value.type !== "event_msg" || !hasString(value, "timestamp")) {
|
|
328
|
+
if (!isRecord$2(value) || value.type !== "event_msg" || !hasString(value, "timestamp")) {
|
|
322
329
|
return false;
|
|
323
330
|
}
|
|
324
331
|
return isEventMsgPayload(value.payload);
|
|
325
332
|
}
|
|
326
333
|
function isCompactedEntry(value) {
|
|
327
|
-
if (!isRecord(value) || value.type !== "compacted" || !hasString(value, "timestamp")) {
|
|
334
|
+
if (!isRecord$2(value) || value.type !== "compacted" || !hasString(value, "timestamp")) {
|
|
328
335
|
return false;
|
|
329
336
|
}
|
|
330
|
-
return isRecord(value.payload);
|
|
337
|
+
return isRecord$2(value.payload);
|
|
331
338
|
}
|
|
332
339
|
function isCompactionEntry(value) {
|
|
333
|
-
if (!isRecord(value) || value.type !== "compaction" || !hasString(value, "timestamp")) {
|
|
340
|
+
if (!isRecord$2(value) || value.type !== "compaction" || !hasString(value, "timestamp")) {
|
|
334
341
|
return false;
|
|
335
342
|
}
|
|
336
|
-
if ("payload" in value && !isRecord(value.payload)) {
|
|
343
|
+
if ("payload" in value && !isRecord$2(value.payload)) {
|
|
337
344
|
return false;
|
|
338
345
|
}
|
|
339
|
-
if ("encrypted_content" in value && !(isString(value.encrypted_content) || value.encrypted_content === null)) {
|
|
346
|
+
if ("encrypted_content" in value && !(isString$2(value.encrypted_content) || value.encrypted_content === null)) {
|
|
340
347
|
return false;
|
|
341
348
|
}
|
|
342
349
|
return true;
|
|
@@ -354,6 +361,40 @@ const EMPTY_CODEX_SESSION_METRICS = {
|
|
|
354
361
|
toolCallCount: 0,
|
|
355
362
|
duration: 0
|
|
356
363
|
};
|
|
364
|
+
function diffTokenUsage(previous, current) {
|
|
365
|
+
const delta = {
|
|
366
|
+
input_tokens: current.input_tokens - previous.input_tokens,
|
|
367
|
+
cached_input_tokens: current.cached_input_tokens - previous.cached_input_tokens,
|
|
368
|
+
output_tokens: current.output_tokens - previous.output_tokens,
|
|
369
|
+
reasoning_output_tokens: current.reasoning_output_tokens - previous.reasoning_output_tokens,
|
|
370
|
+
total_tokens: current.total_tokens - previous.total_tokens
|
|
371
|
+
};
|
|
372
|
+
const normalized = {
|
|
373
|
+
input_tokens: Math.max(delta.input_tokens, 0),
|
|
374
|
+
cached_input_tokens: Math.max(delta.cached_input_tokens, 0),
|
|
375
|
+
output_tokens: Math.max(delta.output_tokens, 0),
|
|
376
|
+
reasoning_output_tokens: Math.max(delta.reasoning_output_tokens, 0),
|
|
377
|
+
total_tokens: Math.max(delta.total_tokens, 0)
|
|
378
|
+
};
|
|
379
|
+
const hasAnyPositive = normalized.input_tokens > 0 || normalized.cached_input_tokens > 0 || normalized.output_tokens > 0 || normalized.reasoning_output_tokens > 0 || normalized.total_tokens > 0;
|
|
380
|
+
return hasAnyPositive ? normalized : null;
|
|
381
|
+
}
|
|
382
|
+
function isSameTokenUsage(left, right) {
|
|
383
|
+
return left.input_tokens === right.input_tokens && left.cached_input_tokens === right.cached_input_tokens && left.output_tokens === right.output_tokens && left.reasoning_output_tokens === right.reasoning_output_tokens && left.total_tokens === right.total_tokens;
|
|
384
|
+
}
|
|
385
|
+
function resolveTokenUsage(previousTotalUsage, currentTotalUsage, fallbackUsage) {
|
|
386
|
+
if (!previousTotalUsage) {
|
|
387
|
+
return fallbackUsage;
|
|
388
|
+
}
|
|
389
|
+
const delta = diffTokenUsage(previousTotalUsage, currentTotalUsage);
|
|
390
|
+
if (delta) {
|
|
391
|
+
return delta;
|
|
392
|
+
}
|
|
393
|
+
if (isSameTokenUsage(previousTotalUsage, currentTotalUsage)) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
return fallbackUsage;
|
|
397
|
+
}
|
|
357
398
|
const AGENTS_HEADING_PATTERN = /^#?\s*AGENTS\.md instructions\b/i;
|
|
358
399
|
const AGENTS_INSTRUCTIONS_BLOCK_PATTERN = /<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>/i;
|
|
359
400
|
const ENVIRONMENT_CONTEXT_WRAPPER_PATTERN = /^<environment_context>[\s\S]*<\/environment_context>$/i;
|
|
@@ -737,10 +778,10 @@ function pickPreferredEquivalentUserContent(pending, incoming) {
|
|
|
737
778
|
}
|
|
738
779
|
return pending;
|
|
739
780
|
}
|
|
740
|
-
function normalizeModel$
|
|
781
|
+
function normalizeModel$3(model) {
|
|
741
782
|
return model?.trim() ?? "";
|
|
742
783
|
}
|
|
743
|
-
function normalizeReasoningEffort$
|
|
784
|
+
function normalizeReasoningEffort$3(effort) {
|
|
744
785
|
const value = effort?.trim();
|
|
745
786
|
return value ? value : "unknown";
|
|
746
787
|
}
|
|
@@ -997,6 +1038,7 @@ class CodexChunkBuilder {
|
|
|
997
1038
|
let pendingUser = null;
|
|
998
1039
|
let lastSeenModelUsage = null;
|
|
999
1040
|
let lastSeenCollaborationMode = "";
|
|
1041
|
+
let previousTotalUsage = null;
|
|
1000
1042
|
const flushAIChunk = () => {
|
|
1001
1043
|
if (!currentAI) {
|
|
1002
1044
|
return;
|
|
@@ -1077,11 +1119,11 @@ class CodexChunkBuilder {
|
|
|
1077
1119
|
continue;
|
|
1078
1120
|
}
|
|
1079
1121
|
if (isTurnContextEntry(entry)) {
|
|
1080
|
-
const model = normalizeModel$
|
|
1122
|
+
const model = normalizeModel$3(entry.payload.model);
|
|
1081
1123
|
if (model) {
|
|
1082
1124
|
const usage = {
|
|
1083
1125
|
model,
|
|
1084
|
-
reasoningEffort: normalizeReasoningEffort$
|
|
1126
|
+
reasoningEffort: normalizeReasoningEffort$3(entry.payload.effort)
|
|
1085
1127
|
};
|
|
1086
1128
|
if (lastSeenModelUsage === null) {
|
|
1087
1129
|
lastSeenModelUsage = usage;
|
|
@@ -1251,30 +1293,33 @@ class CodexChunkBuilder {
|
|
|
1251
1293
|
addReasoningSection(ai, "event", [entry.payload.text]);
|
|
1252
1294
|
continue;
|
|
1253
1295
|
}
|
|
1254
|
-
if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload)) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1296
|
+
if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload) && entry.payload.info) {
|
|
1297
|
+
const currentTotalUsage = entry.payload.info.total_token_usage;
|
|
1298
|
+
const usage = resolveTokenUsage(
|
|
1299
|
+
previousTotalUsage,
|
|
1300
|
+
currentTotalUsage,
|
|
1301
|
+
entry.payload.info.last_token_usage
|
|
1302
|
+
);
|
|
1303
|
+
previousTotalUsage = currentTotalUsage;
|
|
1304
|
+
if (!usage) {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
this.accumulateMetricsFromTokenUsage(ai.metrics, usage);
|
|
1308
|
+
this.assignTokenUsageToPendingTool(ai, usage);
|
|
1257
1309
|
}
|
|
1258
1310
|
}
|
|
1259
1311
|
flushPendingEventUser();
|
|
1260
1312
|
flushAIChunk();
|
|
1261
1313
|
return chunks;
|
|
1262
1314
|
}
|
|
1263
|
-
|
|
1264
|
-
if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
const usage = entry.payload.info.last_token_usage;
|
|
1315
|
+
accumulateMetricsFromTokenUsage(target, usage) {
|
|
1268
1316
|
target.inputTokens = (target.inputTokens ?? 0) + usage.input_tokens;
|
|
1269
1317
|
target.cachedTokens = (target.cachedTokens ?? 0) + usage.cached_input_tokens;
|
|
1270
1318
|
target.outputTokens = (target.outputTokens ?? 0) + usage.output_tokens;
|
|
1271
1319
|
target.reasoningTokens = (target.reasoningTokens ?? 0) + usage.reasoning_output_tokens;
|
|
1272
1320
|
target.totalTokens = (target.totalTokens ?? 0) + usage.total_tokens;
|
|
1273
1321
|
}
|
|
1274
|
-
assignTokenUsageToPendingTool(ai,
|
|
1275
|
-
if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1322
|
+
assignTokenUsageToPendingTool(ai, usage) {
|
|
1278
1323
|
if (ai.pendingUsageToolIndex === null) {
|
|
1279
1324
|
return;
|
|
1280
1325
|
}
|
|
@@ -1283,20 +1328,399 @@ class CodexChunkBuilder {
|
|
|
1283
1328
|
ai.pendingUsageToolIndex = null;
|
|
1284
1329
|
return;
|
|
1285
1330
|
}
|
|
1286
|
-
const usage = entry.payload.info.last_token_usage;
|
|
1287
1331
|
if (tool.tokenUsage) {
|
|
1288
1332
|
tool.tokenUsage.inputTokens += usage.input_tokens;
|
|
1333
|
+
tool.tokenUsage.cachedInputTokens += usage.cached_input_tokens;
|
|
1289
1334
|
tool.tokenUsage.outputTokens += usage.output_tokens;
|
|
1290
1335
|
} else {
|
|
1291
1336
|
tool.tokenUsage = {
|
|
1292
1337
|
inputTokens: usage.input_tokens,
|
|
1338
|
+
cachedInputTokens: usage.cached_input_tokens,
|
|
1293
1339
|
outputTokens: usage.output_tokens
|
|
1294
1340
|
};
|
|
1295
1341
|
}
|
|
1296
1342
|
ai.pendingUsageToolIndex = null;
|
|
1297
1343
|
}
|
|
1298
1344
|
}
|
|
1299
|
-
|
|
1345
|
+
function normalizeRateKey(model) {
|
|
1346
|
+
return model.trim().toLowerCase();
|
|
1347
|
+
}
|
|
1348
|
+
function createZeroTokenTotals() {
|
|
1349
|
+
return {
|
|
1350
|
+
totalTokens: 0,
|
|
1351
|
+
inputTokens: 0,
|
|
1352
|
+
outputTokens: 0,
|
|
1353
|
+
cachedTokens: 0,
|
|
1354
|
+
reasoningTokens: 0
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function addTokenTotals(target, source) {
|
|
1358
|
+
target.totalTokens += source.totalTokens;
|
|
1359
|
+
target.inputTokens += source.inputTokens;
|
|
1360
|
+
target.outputTokens += source.outputTokens;
|
|
1361
|
+
target.cachedTokens += source.cachedTokens;
|
|
1362
|
+
target.reasoningTokens += source.reasoningTokens;
|
|
1363
|
+
}
|
|
1364
|
+
function tokenUsageToTotals(usage) {
|
|
1365
|
+
return {
|
|
1366
|
+
totalTokens: usage.total_tokens,
|
|
1367
|
+
inputTokens: usage.input_tokens,
|
|
1368
|
+
outputTokens: usage.output_tokens,
|
|
1369
|
+
cachedTokens: usage.cached_input_tokens,
|
|
1370
|
+
reasoningTokens: usage.reasoning_output_tokens
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function parseTimestamp(timestamp) {
|
|
1374
|
+
const value = new Date(timestamp);
|
|
1375
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
1376
|
+
}
|
|
1377
|
+
function localDateKey(date) {
|
|
1378
|
+
const year = date.getFullYear();
|
|
1379
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1380
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1381
|
+
return `${year}-${month}-${day}`;
|
|
1382
|
+
}
|
|
1383
|
+
function localHour(date) {
|
|
1384
|
+
return date.getHours();
|
|
1385
|
+
}
|
|
1386
|
+
function ensureDailyBucket(buckets, date) {
|
|
1387
|
+
const existing = buckets.get(date);
|
|
1388
|
+
if (existing) {
|
|
1389
|
+
return existing;
|
|
1390
|
+
}
|
|
1391
|
+
const next = {
|
|
1392
|
+
date,
|
|
1393
|
+
eventCount: 0,
|
|
1394
|
+
...createZeroTokenTotals()
|
|
1395
|
+
};
|
|
1396
|
+
buckets.set(date, next);
|
|
1397
|
+
return next;
|
|
1398
|
+
}
|
|
1399
|
+
function ensureHourlyBucket(buckets, hour) {
|
|
1400
|
+
const existing = buckets.get(hour);
|
|
1401
|
+
if (existing) {
|
|
1402
|
+
return existing;
|
|
1403
|
+
}
|
|
1404
|
+
const next = {
|
|
1405
|
+
hour,
|
|
1406
|
+
eventCount: 0,
|
|
1407
|
+
...createZeroTokenTotals()
|
|
1408
|
+
};
|
|
1409
|
+
buckets.set(hour, next);
|
|
1410
|
+
return next;
|
|
1411
|
+
}
|
|
1412
|
+
function ensureModelBucket(buckets, model, reasoningEffort) {
|
|
1413
|
+
const key = `${normalizeRateKey(model)}::${normalizeReasoningEffort$2(reasoningEffort)}`;
|
|
1414
|
+
const existing = buckets.get(key);
|
|
1415
|
+
if (existing) {
|
|
1416
|
+
return existing;
|
|
1417
|
+
}
|
|
1418
|
+
const next = {
|
|
1419
|
+
model,
|
|
1420
|
+
reasoningEffort,
|
|
1421
|
+
...createZeroTokenTotals()
|
|
1422
|
+
};
|
|
1423
|
+
buckets.set(key, next);
|
|
1424
|
+
return next;
|
|
1425
|
+
}
|
|
1426
|
+
function estimateUsageCostUsd(tokens, rate) {
|
|
1427
|
+
const nonReasoningOutput = Math.max(tokens.outputTokens - tokens.reasoningTokens, 0);
|
|
1428
|
+
const outputCost = nonReasoningOutput / 1e6 * rate.outputUsdPer1M;
|
|
1429
|
+
const reasoningCost = tokens.reasoningTokens / 1e6 * rate.reasoningOutputUsdPer1M;
|
|
1430
|
+
return tokens.inputTokens / 1e6 * rate.inputUsdPer1M + tokens.cachedTokens / 1e6 * rate.cachedInputUsdPer1M + outputCost + reasoningCost;
|
|
1431
|
+
}
|
|
1432
|
+
function buildRateLookup(rateCard) {
|
|
1433
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
1434
|
+
for (const rate of rateCard.models) {
|
|
1435
|
+
lookup.set(normalizeRateKey(rate.model), rate);
|
|
1436
|
+
}
|
|
1437
|
+
return lookup;
|
|
1438
|
+
}
|
|
1439
|
+
function buildHourlyResult(hourly) {
|
|
1440
|
+
const result = [];
|
|
1441
|
+
for (let hour = 0; hour <= 23; hour += 1) {
|
|
1442
|
+
const current = hourly.get(hour);
|
|
1443
|
+
if (!current) {
|
|
1444
|
+
result.push({
|
|
1445
|
+
hour,
|
|
1446
|
+
eventCount: 0,
|
|
1447
|
+
sessionCount: 0,
|
|
1448
|
+
...createZeroTokenTotals()
|
|
1449
|
+
});
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
result.push({
|
|
1453
|
+
hour,
|
|
1454
|
+
eventCount: current.eventCount,
|
|
1455
|
+
sessionCount: current.sessions.size,
|
|
1456
|
+
totalTokens: current.totalTokens,
|
|
1457
|
+
inputTokens: current.inputTokens,
|
|
1458
|
+
outputTokens: current.outputTokens,
|
|
1459
|
+
cachedTokens: current.cachedTokens,
|
|
1460
|
+
reasoningTokens: current.reasoningTokens
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return result;
|
|
1464
|
+
}
|
|
1465
|
+
function normalizeModel$2(model) {
|
|
1466
|
+
const trimmed = model?.trim();
|
|
1467
|
+
return trimmed && trimmed.length > 0 ? trimmed : "unknown-model";
|
|
1468
|
+
}
|
|
1469
|
+
function normalizeReasoningEffort$2(reasoningEffort) {
|
|
1470
|
+
const trimmed = reasoningEffort?.trim();
|
|
1471
|
+
return trimmed && trimmed.length > 0 ? trimmed : "unknown";
|
|
1472
|
+
}
|
|
1473
|
+
function buildSessionStatsRecord(parsed, revision, nowIso) {
|
|
1474
|
+
const dailyBuckets = /* @__PURE__ */ new Map();
|
|
1475
|
+
const hourlyBuckets = /* @__PURE__ */ new Map();
|
|
1476
|
+
const modelBuckets = /* @__PURE__ */ new Map();
|
|
1477
|
+
const tokens = createZeroTokenTotals();
|
|
1478
|
+
let eventCount = 0;
|
|
1479
|
+
let currentModel = normalizeModel$2(parsed.session.model || parsed.sessionMeta?.payload.model);
|
|
1480
|
+
let currentReasoningEffort = "unknown";
|
|
1481
|
+
let previousTotalUsage = null;
|
|
1482
|
+
for (const modelUsage of parsed.session.modelUsages) {
|
|
1483
|
+
if (modelUsage.model) {
|
|
1484
|
+
currentModel = normalizeModel$2(modelUsage.model);
|
|
1485
|
+
currentReasoningEffort = normalizeReasoningEffort$2(modelUsage.reasoningEffort);
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
for (const entry of parsed.entries) {
|
|
1490
|
+
if (isTurnContextEntry(entry)) {
|
|
1491
|
+
if (entry.payload.model) {
|
|
1492
|
+
currentModel = normalizeModel$2(entry.payload.model);
|
|
1493
|
+
}
|
|
1494
|
+
currentReasoningEffort = normalizeReasoningEffort$2(entry.payload.effort);
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (!isEventMsgEntry(entry) || !isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const timestamp = parseTimestamp(entry.timestamp);
|
|
1501
|
+
const currentTotal = entry.payload.info.total_token_usage;
|
|
1502
|
+
const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
|
|
1503
|
+
previousTotalUsage = currentTotal;
|
|
1504
|
+
if (!usage) {
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const usageTotals = tokenUsageToTotals(usage);
|
|
1508
|
+
addTokenTotals(tokens, usageTotals);
|
|
1509
|
+
const modelTotals = ensureModelBucket(modelBuckets, currentModel, currentReasoningEffort);
|
|
1510
|
+
addTokenTotals(modelTotals, usageTotals);
|
|
1511
|
+
if (!timestamp) {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
|
|
1515
|
+
addTokenTotals(dateBucket, usageTotals);
|
|
1516
|
+
const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
|
|
1517
|
+
addTokenTotals(hourBucket, usageTotals);
|
|
1518
|
+
}
|
|
1519
|
+
for (const classifiedEntry of parsed.classifiedEntries) {
|
|
1520
|
+
const timestamp = parseTimestamp(classifiedEntry.entry.timestamp);
|
|
1521
|
+
if (!timestamp) {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
eventCount += 1;
|
|
1525
|
+
const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
|
|
1526
|
+
dateBucket.eventCount += 1;
|
|
1527
|
+
const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
|
|
1528
|
+
hourBucket.eventCount += 1;
|
|
1529
|
+
}
|
|
1530
|
+
const lastActivity = parsed.entries[parsed.entries.length - 1]?.timestamp ?? parsed.session.startTime;
|
|
1531
|
+
return {
|
|
1532
|
+
tokenComputationVersion: 4,
|
|
1533
|
+
sessionId: parsed.session.id,
|
|
1534
|
+
filePath: parsed.session.filePath,
|
|
1535
|
+
revision,
|
|
1536
|
+
archived: false,
|
|
1537
|
+
cwd: parsed.session.cwd,
|
|
1538
|
+
startTime: parsed.session.startTime,
|
|
1539
|
+
lastActivity,
|
|
1540
|
+
modelUsages: parsed.session.modelUsages,
|
|
1541
|
+
eventCount,
|
|
1542
|
+
turnCount: parsed.metrics.turnCount,
|
|
1543
|
+
toolCallCount: parsed.metrics.toolCallCount,
|
|
1544
|
+
durationMs: parsed.metrics.duration,
|
|
1545
|
+
tokens,
|
|
1546
|
+
modelTokenTotals: Array.from(modelBuckets.values()).sort((a, b) => b.totalTokens - a.totalTokens),
|
|
1547
|
+
dailyBuckets: Array.from(dailyBuckets.values()).sort((a, b) => a.date.localeCompare(b.date)),
|
|
1548
|
+
hourlyBuckets: Array.from(hourlyBuckets.values()).sort((a, b) => a.hour - b.hour),
|
|
1549
|
+
lastSeenAt: nowIso
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function aggregateStatsSummary(records, scope, rateCard) {
|
|
1553
|
+
const scopedRecords = records.filter((record) => {
|
|
1554
|
+
if (scope.type === "all") {
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
return record.cwd === scope.cwd;
|
|
1558
|
+
});
|
|
1559
|
+
const daily = /* @__PURE__ */ new Map();
|
|
1560
|
+
const hourly = /* @__PURE__ */ new Map();
|
|
1561
|
+
const modelBreakdown = /* @__PURE__ */ new Map();
|
|
1562
|
+
const rateLookup = buildRateLookup(rateCard);
|
|
1563
|
+
const unpricedModels = /* @__PURE__ */ new Set();
|
|
1564
|
+
const totals = {
|
|
1565
|
+
sessions: 0,
|
|
1566
|
+
archivedSessions: 0,
|
|
1567
|
+
eventCount: 0,
|
|
1568
|
+
durationMs: 0,
|
|
1569
|
+
estimatedCostUsd: 0,
|
|
1570
|
+
...createZeroTokenTotals()
|
|
1571
|
+
};
|
|
1572
|
+
const costCoverage = {
|
|
1573
|
+
pricedTokens: 0,
|
|
1574
|
+
unpricedTokens: 0
|
|
1575
|
+
};
|
|
1576
|
+
for (const record of scopedRecords) {
|
|
1577
|
+
totals.sessions += 1;
|
|
1578
|
+
if (record.archived) {
|
|
1579
|
+
totals.archivedSessions += 1;
|
|
1580
|
+
}
|
|
1581
|
+
totals.eventCount += record.eventCount;
|
|
1582
|
+
totals.durationMs += record.durationMs;
|
|
1583
|
+
addTokenTotals(totals, record.tokens);
|
|
1584
|
+
for (const bucket of record.dailyBuckets) {
|
|
1585
|
+
const existing = daily.get(bucket.date) ?? {
|
|
1586
|
+
date: bucket.date,
|
|
1587
|
+
eventCount: 0,
|
|
1588
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1589
|
+
...createZeroTokenTotals()
|
|
1590
|
+
};
|
|
1591
|
+
existing.eventCount += bucket.eventCount;
|
|
1592
|
+
addTokenTotals(existing, bucket);
|
|
1593
|
+
existing.sessions.add(record.sessionId);
|
|
1594
|
+
daily.set(bucket.date, existing);
|
|
1595
|
+
}
|
|
1596
|
+
for (const bucket of record.hourlyBuckets) {
|
|
1597
|
+
const existing = hourly.get(bucket.hour) ?? {
|
|
1598
|
+
hour: bucket.hour,
|
|
1599
|
+
eventCount: 0,
|
|
1600
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1601
|
+
...createZeroTokenTotals()
|
|
1602
|
+
};
|
|
1603
|
+
existing.eventCount += bucket.eventCount;
|
|
1604
|
+
addTokenTotals(existing, bucket);
|
|
1605
|
+
existing.sessions.add(record.sessionId);
|
|
1606
|
+
hourly.set(bucket.hour, existing);
|
|
1607
|
+
}
|
|
1608
|
+
for (const modelTotals of record.modelTokenTotals) {
|
|
1609
|
+
const rate = rateLookup.get(normalizeRateKey(modelTotals.model));
|
|
1610
|
+
const modelKey = `${normalizeRateKey(modelTotals.model)}::${normalizeReasoningEffort$2(modelTotals.reasoningEffort)}`;
|
|
1611
|
+
const existing = modelBreakdown.get(modelKey) ?? {
|
|
1612
|
+
model: modelTotals.model,
|
|
1613
|
+
reasoningEffort: modelTotals.reasoningEffort,
|
|
1614
|
+
estimatedCostUsd: 0,
|
|
1615
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1616
|
+
archivedSessions: /* @__PURE__ */ new Set(),
|
|
1617
|
+
...createZeroTokenTotals()
|
|
1618
|
+
};
|
|
1619
|
+
addTokenTotals(existing, modelTotals);
|
|
1620
|
+
existing.sessions.add(record.sessionId);
|
|
1621
|
+
if (record.archived) {
|
|
1622
|
+
existing.archivedSessions.add(record.sessionId);
|
|
1623
|
+
}
|
|
1624
|
+
if (rate) {
|
|
1625
|
+
const usageCost = estimateUsageCostUsd(modelTotals, rate);
|
|
1626
|
+
existing.estimatedCostUsd += usageCost;
|
|
1627
|
+
totals.estimatedCostUsd += usageCost;
|
|
1628
|
+
costCoverage.pricedTokens += modelTotals.totalTokens;
|
|
1629
|
+
} else {
|
|
1630
|
+
costCoverage.unpricedTokens += modelTotals.totalTokens;
|
|
1631
|
+
unpricedModels.add(modelTotals.model);
|
|
1632
|
+
}
|
|
1633
|
+
modelBreakdown.set(modelKey, existing);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const dailyPoints = Array.from(daily.values()).map((bucket) => ({
|
|
1637
|
+
date: bucket.date,
|
|
1638
|
+
eventCount: bucket.eventCount,
|
|
1639
|
+
sessionCount: bucket.sessions.size,
|
|
1640
|
+
totalTokens: bucket.totalTokens,
|
|
1641
|
+
inputTokens: bucket.inputTokens,
|
|
1642
|
+
outputTokens: bucket.outputTokens,
|
|
1643
|
+
cachedTokens: bucket.cachedTokens,
|
|
1644
|
+
reasoningTokens: bucket.reasoningTokens
|
|
1645
|
+
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
1646
|
+
const hourlyPoints = buildHourlyResult(hourly);
|
|
1647
|
+
const models = Array.from(modelBreakdown.values()).map((model) => ({
|
|
1648
|
+
model: model.model,
|
|
1649
|
+
reasoningEffort: model.reasoningEffort,
|
|
1650
|
+
sessionCount: model.sessions.size,
|
|
1651
|
+
archivedSessionCount: model.archivedSessions.size,
|
|
1652
|
+
estimatedCostUsd: Number(model.estimatedCostUsd.toFixed(6)),
|
|
1653
|
+
totalTokens: model.totalTokens,
|
|
1654
|
+
inputTokens: model.inputTokens,
|
|
1655
|
+
outputTokens: model.outputTokens,
|
|
1656
|
+
cachedTokens: model.cachedTokens,
|
|
1657
|
+
reasoningTokens: model.reasoningTokens
|
|
1658
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1659
|
+
const reasoningMap = /* @__PURE__ */ new Map();
|
|
1660
|
+
for (const model of modelBreakdown.values()) {
|
|
1661
|
+
const existing = reasoningMap.get(model.reasoningEffort) ?? {
|
|
1662
|
+
reasoningEffort: model.reasoningEffort,
|
|
1663
|
+
estimatedCostUsd: 0,
|
|
1664
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1665
|
+
...createZeroTokenTotals()
|
|
1666
|
+
};
|
|
1667
|
+
for (const sessionId of model.sessions) {
|
|
1668
|
+
existing.sessions.add(sessionId);
|
|
1669
|
+
}
|
|
1670
|
+
existing.estimatedCostUsd += model.estimatedCostUsd;
|
|
1671
|
+
addTokenTotals(existing, model);
|
|
1672
|
+
reasoningMap.set(model.reasoningEffort, existing);
|
|
1673
|
+
}
|
|
1674
|
+
const reasoningEfforts = Array.from(reasoningMap.values()).map((reasoning) => ({
|
|
1675
|
+
reasoningEffort: reasoning.reasoningEffort,
|
|
1676
|
+
sessionCount: reasoning.sessions.size,
|
|
1677
|
+
estimatedCostUsd: Number(reasoning.estimatedCostUsd.toFixed(6)),
|
|
1678
|
+
totalTokens: reasoning.totalTokens,
|
|
1679
|
+
inputTokens: reasoning.inputTokens,
|
|
1680
|
+
outputTokens: reasoning.outputTokens,
|
|
1681
|
+
cachedTokens: reasoning.cachedTokens,
|
|
1682
|
+
reasoningTokens: reasoning.reasoningTokens
|
|
1683
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1684
|
+
const topDays = [...dailyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((day) => ({
|
|
1685
|
+
date: day.date,
|
|
1686
|
+
eventCount: day.eventCount,
|
|
1687
|
+
sessionCount: day.sessionCount,
|
|
1688
|
+
totalTokens: day.totalTokens,
|
|
1689
|
+
outputTokens: day.outputTokens
|
|
1690
|
+
}));
|
|
1691
|
+
const topHours = [...hourlyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((hour) => ({
|
|
1692
|
+
hour: hour.hour,
|
|
1693
|
+
eventCount: hour.eventCount,
|
|
1694
|
+
sessionCount: hour.sessionCount,
|
|
1695
|
+
totalTokens: hour.totalTokens,
|
|
1696
|
+
outputTokens: hour.outputTokens
|
|
1697
|
+
}));
|
|
1698
|
+
return {
|
|
1699
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1700
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "local",
|
|
1701
|
+
scope,
|
|
1702
|
+
totals: {
|
|
1703
|
+
...totals,
|
|
1704
|
+
estimatedCostUsd: Number(totals.estimatedCostUsd.toFixed(6))
|
|
1705
|
+
},
|
|
1706
|
+
daily: dailyPoints,
|
|
1707
|
+
hourly: hourlyPoints,
|
|
1708
|
+
topDays,
|
|
1709
|
+
topHours,
|
|
1710
|
+
models,
|
|
1711
|
+
reasoningEfforts,
|
|
1712
|
+
costCoverage: {
|
|
1713
|
+
pricedTokens: costCoverage.pricedTokens,
|
|
1714
|
+
unpricedTokens: costCoverage.unpricedTokens,
|
|
1715
|
+
unpricedModels: Array.from(unpricedModels.values()).sort((a, b) => a.localeCompare(b))
|
|
1716
|
+
},
|
|
1717
|
+
rates: {
|
|
1718
|
+
updatedAt: rateCard.updatedAt,
|
|
1719
|
+
source: rateCard.source
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
const logger$4 = createLogger("Main:jsonl");
|
|
1300
1724
|
const defaultParser = (value) => value;
|
|
1301
1725
|
function waitForStreamClose(stream) {
|
|
1302
1726
|
if (stream.closed) {
|
|
@@ -1343,7 +1767,7 @@ async function streamJsonlFile(filePath, options) {
|
|
|
1343
1767
|
} catch (error) {
|
|
1344
1768
|
options.onError?.(error, line, lineNumber);
|
|
1345
1769
|
if (!options.onError) {
|
|
1346
|
-
logger$
|
|
1770
|
+
logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1347
1771
|
}
|
|
1348
1772
|
}
|
|
1349
1773
|
}
|
|
@@ -1384,7 +1808,7 @@ async function readFirstJsonlEntry(filePath, parser) {
|
|
|
1384
1808
|
break;
|
|
1385
1809
|
}
|
|
1386
1810
|
} catch (error) {
|
|
1387
|
-
logger$
|
|
1811
|
+
logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1388
1812
|
}
|
|
1389
1813
|
}
|
|
1390
1814
|
} finally {
|
|
@@ -1392,7 +1816,7 @@ async function readFirstJsonlEntry(filePath, parser) {
|
|
|
1392
1816
|
}
|
|
1393
1817
|
return firstEntry;
|
|
1394
1818
|
}
|
|
1395
|
-
const logger$
|
|
1819
|
+
const logger$3 = createLogger("Service:CodexSessionScanner");
|
|
1396
1820
|
function isWithinDateRange(timestamp, options) {
|
|
1397
1821
|
const current = new Date(timestamp).getTime();
|
|
1398
1822
|
if (Number.isNaN(current)) {
|
|
@@ -1438,7 +1862,7 @@ class CodexSessionScanner {
|
|
|
1438
1862
|
(value) => isSessionMetaEntry(value) ? value : null
|
|
1439
1863
|
);
|
|
1440
1864
|
if (!metaEntry) {
|
|
1441
|
-
logger$
|
|
1865
|
+
logger$3.warn(`Skipping session file without valid session_meta: ${filePath}`);
|
|
1442
1866
|
continue;
|
|
1443
1867
|
}
|
|
1444
1868
|
if (!isWithinDateRange(metaEntry.timestamp, options)) {
|
|
@@ -1575,6 +1999,7 @@ class CodexSessionParser {
|
|
|
1575
1999
|
let firstTimestamp = null;
|
|
1576
2000
|
let lastTimestamp = null;
|
|
1577
2001
|
let firstTurnContextModel = null;
|
|
2002
|
+
let previousTotalUsage = null;
|
|
1578
2003
|
const modelUsages = [];
|
|
1579
2004
|
const modelUsageKeys = /* @__PURE__ */ new Set();
|
|
1580
2005
|
for (const entry of entries) {
|
|
@@ -1615,7 +2040,12 @@ class CodexSessionParser {
|
|
|
1615
2040
|
if (isEventMsgEntry(entry)) {
|
|
1616
2041
|
eventMessages.push(entry);
|
|
1617
2042
|
if (isTokenCountPayload(entry.payload) && entry.payload.info) {
|
|
1618
|
-
const
|
|
2043
|
+
const currentTotal = entry.payload.info.total_token_usage;
|
|
2044
|
+
const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
|
|
2045
|
+
previousTotalUsage = currentTotal;
|
|
2046
|
+
if (!usage) {
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
1619
2049
|
metrics.inputTokens += usage.input_tokens;
|
|
1620
2050
|
metrics.cachedTokens += usage.cached_input_tokens;
|
|
1621
2051
|
metrics.outputTokens += usage.output_tokens;
|
|
@@ -1713,7 +2143,7 @@ class DataCache {
|
|
|
1713
2143
|
return `${cwdHash}/${sessionId}`;
|
|
1714
2144
|
}
|
|
1715
2145
|
}
|
|
1716
|
-
const logger = createLogger("Service:FileWatcher");
|
|
2146
|
+
const logger$2 = createLogger("Service:FileWatcher");
|
|
1717
2147
|
const DEBOUNCE_MS = 100;
|
|
1718
2148
|
class FileWatcher extends node_events.EventEmitter {
|
|
1719
2149
|
constructor(sessionsPath = path__namespace.join(os__namespace.homedir(), ".codex", "sessions")) {
|
|
@@ -1739,7 +2169,7 @@ class FileWatcher extends node_events.EventEmitter {
|
|
|
1739
2169
|
}
|
|
1740
2170
|
);
|
|
1741
2171
|
} catch (error) {
|
|
1742
|
-
logger.error(`Failed to start watcher for ${this.sessionsPath}`, error);
|
|
2172
|
+
logger$2.error(`Failed to start watcher for ${this.sessionsPath}`, error);
|
|
1743
2173
|
}
|
|
1744
2174
|
}
|
|
1745
2175
|
stop() {
|
|
@@ -1787,10 +2217,179 @@ class FileWatcher extends node_events.EventEmitter {
|
|
|
1787
2217
|
return fs__namespace.existsSync(targetPath) ? "created" : "deleted";
|
|
1788
2218
|
}
|
|
1789
2219
|
}
|
|
1790
|
-
const
|
|
1791
|
-
const
|
|
2220
|
+
const logger$1 = createLogger("Service:ModelRatesStore");
|
|
2221
|
+
const RATES_VERSION = 1;
|
|
2222
|
+
const BUNDLED_DEFAULT_RATES = [
|
|
2223
|
+
{ model: "gpt-5.2", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
|
|
2224
|
+
{ model: "gpt-5.1", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2225
|
+
{ model: "gpt-5", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2226
|
+
{ model: "gpt-5-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
|
|
2227
|
+
{ model: "gpt-5-nano", inputUsdPer1M: 0.05, cachedInputUsdPer1M: 5e-3, outputUsdPer1M: 0.4, reasoningOutputUsdPer1M: 0.4 },
|
|
2228
|
+
{ model: "gpt-5.2-codex", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
|
|
2229
|
+
{ model: "gpt-5.1-codex-max", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2230
|
+
{ model: "gpt-5.1-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2231
|
+
{ model: "gpt-5-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2232
|
+
{ model: "gpt-5.1-codex-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
|
|
2233
|
+
{ model: "codex-mini-latest", inputUsdPer1M: 1.5, cachedInputUsdPer1M: 0.375, outputUsdPer1M: 6, reasoningOutputUsdPer1M: 6 }
|
|
2234
|
+
];
|
|
2235
|
+
const DEFAULT_RATE_CARD = {
|
|
2236
|
+
updatedAt: null,
|
|
2237
|
+
source: "bundled-defaults",
|
|
2238
|
+
models: BUNDLED_DEFAULT_RATES,
|
|
2239
|
+
warnings: ["Bundled rates may be outdated. Pricing refresh is currently disabled."]
|
|
2240
|
+
};
|
|
2241
|
+
function isRecord$1(value) {
|
|
2242
|
+
return typeof value === "object" && value !== null;
|
|
2243
|
+
}
|
|
2244
|
+
function isString$1(value) {
|
|
2245
|
+
return typeof value === "string";
|
|
2246
|
+
}
|
|
2247
|
+
function isFiniteNumber$1(value) {
|
|
2248
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
2249
|
+
}
|
|
2250
|
+
function isCodexModelRate(value) {
|
|
2251
|
+
return isRecord$1(value) && isString$1(value.model) && isFiniteNumber$1(value.inputUsdPer1M) && isFiniteNumber$1(value.cachedInputUsdPer1M) && isFiniteNumber$1(value.outputUsdPer1M) && isFiniteNumber$1(value.reasoningOutputUsdPer1M);
|
|
2252
|
+
}
|
|
2253
|
+
function isRatesFile(value) {
|
|
2254
|
+
return isRecord$1(value) && isFiniteNumber$1(value.version) && (value.updatedAt === null || isString$1(value.updatedAt)) && (value.source === null || isString$1(value.source)) && Array.isArray(value.models) && value.models.every(isCodexModelRate) && Array.isArray(value.warnings) && value.warnings.every(isString$1);
|
|
2255
|
+
}
|
|
2256
|
+
function cloneRateCard(card) {
|
|
2257
|
+
return structuredClone(card);
|
|
2258
|
+
}
|
|
2259
|
+
class ModelRatesStore {
|
|
2260
|
+
constructor(filePath) {
|
|
2261
|
+
this.filePath = filePath;
|
|
2262
|
+
}
|
|
2263
|
+
async getRateCard() {
|
|
2264
|
+
const file = await this.readRateFile();
|
|
2265
|
+
return {
|
|
2266
|
+
updatedAt: file.updatedAt,
|
|
2267
|
+
source: file.source,
|
|
2268
|
+
models: file.models,
|
|
2269
|
+
warnings: file.warnings
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
async refreshFromPricingPage() {
|
|
2273
|
+
const current = await this.getRateCard();
|
|
2274
|
+
return {
|
|
2275
|
+
...current,
|
|
2276
|
+
warnings: [...current.warnings, "Pricing refresh is disabled in this build."],
|
|
2277
|
+
refreshed: false
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
async readRateFile() {
|
|
2281
|
+
try {
|
|
2282
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
2283
|
+
const parsed = JSON.parse(raw);
|
|
2284
|
+
if (!isRatesFile(parsed) || parsed.version !== RATES_VERSION) {
|
|
2285
|
+
return {
|
|
2286
|
+
version: RATES_VERSION,
|
|
2287
|
+
updatedAt: DEFAULT_RATE_CARD.updatedAt,
|
|
2288
|
+
source: DEFAULT_RATE_CARD.source,
|
|
2289
|
+
models: cloneRateCard(DEFAULT_RATE_CARD).models,
|
|
2290
|
+
warnings: [...DEFAULT_RATE_CARD.warnings]
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
return parsed;
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
if (error.code !== "ENOENT") {
|
|
2296
|
+
logger$1.warn("Failed to load model rates, falling back to bundled defaults", error);
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
version: RATES_VERSION,
|
|
2300
|
+
updatedAt: DEFAULT_RATE_CARD.updatedAt,
|
|
2301
|
+
source: DEFAULT_RATE_CARD.source,
|
|
2302
|
+
models: cloneRateCard(DEFAULT_RATE_CARD).models,
|
|
2303
|
+
warnings: [...DEFAULT_RATE_CARD.warnings]
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const logger = createLogger("Service:StatsSnapshotStore");
|
|
2309
|
+
const SNAPSHOT_VERSION = 1;
|
|
2310
|
+
function isRecord(value) {
|
|
2311
|
+
return typeof value === "object" && value !== null;
|
|
2312
|
+
}
|
|
2313
|
+
function isFiniteNumber(value) {
|
|
2314
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
2315
|
+
}
|
|
2316
|
+
function isString(value) {
|
|
2317
|
+
return typeof value === "string";
|
|
2318
|
+
}
|
|
2319
|
+
function isTokenTotals(value) {
|
|
2320
|
+
return isRecord(value) && isFiniteNumber(value.totalTokens) && isFiniteNumber(value.inputTokens) && isFiniteNumber(value.outputTokens) && isFiniteNumber(value.cachedTokens) && isFiniteNumber(value.reasoningTokens);
|
|
2321
|
+
}
|
|
2322
|
+
function isDailyBucket(value) {
|
|
2323
|
+
return isRecord(value) && isString(value.date) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
|
|
2324
|
+
}
|
|
2325
|
+
function isHourlyBucket(value) {
|
|
2326
|
+
return isRecord(value) && isFiniteNumber(value.hour) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
|
|
2327
|
+
}
|
|
2328
|
+
function isModelTotals(value) {
|
|
2329
|
+
return isRecord(value) && isString(value.model) && isString(value.reasoningEffort) && isTokenTotals(value);
|
|
2330
|
+
}
|
|
2331
|
+
function isSessionModelUsage(value) {
|
|
2332
|
+
return isRecord(value) && isString(value.model) && isString(value.reasoningEffort);
|
|
2333
|
+
}
|
|
2334
|
+
function isSessionRecord(value) {
|
|
2335
|
+
return isRecord(value) && (!("tokenComputationVersion" in value) || isFiniteNumber(value.tokenComputationVersion)) && isString(value.sessionId) && isString(value.filePath) && isString(value.revision) && typeof value.archived === "boolean" && isString(value.cwd) && isString(value.startTime) && isString(value.lastActivity) && isFiniteNumber(value.eventCount) && isFiniteNumber(value.turnCount) && isFiniteNumber(value.toolCallCount) && isFiniteNumber(value.durationMs) && Array.isArray(value.modelUsages) && value.modelUsages.every(isSessionModelUsage) && isTokenTotals(value.tokens) && Array.isArray(value.modelTokenTotals) && value.modelTokenTotals.every(isModelTotals) && Array.isArray(value.dailyBuckets) && value.dailyBuckets.every(isDailyBucket) && Array.isArray(value.hourlyBuckets) && value.hourlyBuckets.every(isHourlyBucket) && isString(value.lastSeenAt);
|
|
2336
|
+
}
|
|
2337
|
+
function isSnapshotFile(value) {
|
|
2338
|
+
return isRecord(value) && isFiniteNumber(value.version) && Array.isArray(value.sessions) && value.sessions.every(isSessionRecord);
|
|
2339
|
+
}
|
|
2340
|
+
function cloneSessions(rows) {
|
|
2341
|
+
return structuredClone(rows);
|
|
2342
|
+
}
|
|
2343
|
+
class StatsSnapshotStore {
|
|
2344
|
+
constructor(filePath) {
|
|
2345
|
+
this.filePath = filePath;
|
|
2346
|
+
}
|
|
2347
|
+
async getSessions() {
|
|
2348
|
+
const snapshot = await this.readSnapshot();
|
|
2349
|
+
return cloneSessions(snapshot.sessions);
|
|
2350
|
+
}
|
|
2351
|
+
async saveSessions(rows) {
|
|
2352
|
+
const snapshot = {
|
|
2353
|
+
version: SNAPSHOT_VERSION,
|
|
2354
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2355
|
+
sessions: cloneSessions(rows)
|
|
2356
|
+
};
|
|
2357
|
+
const directory = path__namespace.dirname(this.filePath);
|
|
2358
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
2359
|
+
const tempPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2360
|
+
await fs.promises.writeFile(tempPath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2361
|
+
await fs.promises.rename(tempPath, this.filePath);
|
|
2362
|
+
}
|
|
2363
|
+
async readSnapshot() {
|
|
2364
|
+
try {
|
|
2365
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
2366
|
+
const parsed = JSON.parse(raw);
|
|
2367
|
+
if (!isSnapshotFile(parsed) || parsed.version !== SNAPSHOT_VERSION) {
|
|
2368
|
+
return {
|
|
2369
|
+
version: SNAPSHOT_VERSION,
|
|
2370
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2371
|
+
sessions: []
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
return parsed;
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
if (error.code !== "ENOENT") {
|
|
2377
|
+
logger.warn("Failed to read stats snapshot, using empty snapshot", error);
|
|
2378
|
+
}
|
|
2379
|
+
return {
|
|
2380
|
+
version: SNAPSHOT_VERSION,
|
|
2381
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2382
|
+
sessions: []
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const DETAIL_CACHE_PREFIX = "detail-v3";
|
|
2388
|
+
const CHUNKS_CACHE_PREFIX = "chunks-v4";
|
|
1792
2389
|
const SESSIONS_CACHE_PREFIX = "sessions";
|
|
2390
|
+
const STATS_CACHE_PREFIX = "stats-v3";
|
|
1793
2391
|
const UNKNOWN_REVISION = "unknown-revision";
|
|
2392
|
+
const TOKEN_COMPUTATION_VERSION = 4;
|
|
1794
2393
|
function hasCompactionSignals(entries) {
|
|
1795
2394
|
return entries.some(
|
|
1796
2395
|
(entry) => isCompactedEntry(entry) || isCompactionEntry(entry) || isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)
|
|
@@ -1857,15 +2456,53 @@ function buildProjectsFromSessions(sessions) {
|
|
|
1857
2456
|
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
1858
2457
|
);
|
|
1859
2458
|
}
|
|
2459
|
+
function normalizeStatsScope(scope) {
|
|
2460
|
+
if (!scope || scope.type === "all") {
|
|
2461
|
+
return { type: "all" };
|
|
2462
|
+
}
|
|
2463
|
+
const cwd = scope.cwd?.trim();
|
|
2464
|
+
if (!cwd) {
|
|
2465
|
+
return { type: "all" };
|
|
2466
|
+
}
|
|
2467
|
+
return {
|
|
2468
|
+
type: "project",
|
|
2469
|
+
cwd
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function serializeStatsScope(scope) {
|
|
2473
|
+
if (scope.type === "all") {
|
|
2474
|
+
return "all";
|
|
2475
|
+
}
|
|
2476
|
+
return `project:${scope.cwd}`;
|
|
2477
|
+
}
|
|
2478
|
+
function buildStatsSessionSignature(sessions, revisionByFilePath) {
|
|
2479
|
+
const hash = node_crypto.createHash("sha1");
|
|
2480
|
+
const sortedSessions = [...sessions].sort(
|
|
2481
|
+
(left, right) => left.filePath.localeCompare(right.filePath) || left.id.localeCompare(right.id)
|
|
2482
|
+
);
|
|
2483
|
+
for (const session of sortedSessions) {
|
|
2484
|
+
hash.update(session.id);
|
|
2485
|
+
hash.update("\0");
|
|
2486
|
+
hash.update(session.filePath);
|
|
2487
|
+
hash.update("\0");
|
|
2488
|
+
hash.update(revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION);
|
|
2489
|
+
hash.update("\n");
|
|
2490
|
+
}
|
|
2491
|
+
return hash.digest("hex");
|
|
2492
|
+
}
|
|
1860
2493
|
class CodexServiceContext {
|
|
1861
2494
|
constructor(options = {}) {
|
|
1862
2495
|
const sessionsPath = options.sessionsPath ?? process.env.CODEX_SESSIONS_PATH;
|
|
2496
|
+
const configPath = options.configPath ?? DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH;
|
|
1863
2497
|
this.scanner = new CodexSessionScanner(sessionsPath);
|
|
1864
2498
|
this.parser = new CodexSessionParser();
|
|
1865
2499
|
this.chunkBuilder = new CodexChunkBuilder();
|
|
1866
2500
|
this.dataCache = new DataCache(options.cacheSize ?? 200, options.cacheTtlMinutes ?? 10);
|
|
1867
2501
|
this.watcher = new FileWatcher(sessionsPath);
|
|
1868
|
-
this.configManager = new ConfigManager(
|
|
2502
|
+
this.configManager = new ConfigManager(configPath);
|
|
2503
|
+
const metadataDirectory = path__namespace.dirname(configPath);
|
|
2504
|
+
this.statsSnapshotStore = new StatsSnapshotStore(path__namespace.join(metadataDirectory, "stats-snapshot.json"));
|
|
2505
|
+
this.modelRatesStore = new ModelRatesStore(path__namespace.join(metadataDirectory, "stats-rates.json"));
|
|
1869
2506
|
this.removeFileChangeListener = this.watcher.onFileChange(() => {
|
|
1870
2507
|
this.dataCache.clear();
|
|
1871
2508
|
});
|
|
@@ -1983,6 +2620,33 @@ class CodexServiceContext {
|
|
|
1983
2620
|
results
|
|
1984
2621
|
};
|
|
1985
2622
|
}
|
|
2623
|
+
async getModelRates() {
|
|
2624
|
+
return this.modelRatesStore.getRateCard();
|
|
2625
|
+
}
|
|
2626
|
+
async refreshModelRates() {
|
|
2627
|
+
const refreshed = await this.modelRatesStore.refreshFromPricingPage();
|
|
2628
|
+
this.dataCache.clear();
|
|
2629
|
+
return refreshed;
|
|
2630
|
+
}
|
|
2631
|
+
async getStats(scope = { type: "all" }) {
|
|
2632
|
+
const normalizedScope = normalizeStatsScope(scope);
|
|
2633
|
+
const rates = await this.getModelRates();
|
|
2634
|
+
const sessions = await this.scanner.scanSessions();
|
|
2635
|
+
const revisionByFilePath = await this.getSessionRevisionLookup(sessions);
|
|
2636
|
+
const sessionSignature = buildStatsSessionSignature(sessions, revisionByFilePath);
|
|
2637
|
+
const cacheKey = DataCache.buildKey(
|
|
2638
|
+
STATS_CACHE_PREFIX,
|
|
2639
|
+
`${serializeStatsScope(normalizedScope)}:${rates.updatedAt ?? "never"}:${sessionSignature}`
|
|
2640
|
+
);
|
|
2641
|
+
const cached = this.dataCache.get(cacheKey);
|
|
2642
|
+
if (cached) {
|
|
2643
|
+
return cached;
|
|
2644
|
+
}
|
|
2645
|
+
const reconciledRecords = await this.reconcileStatsSnapshot(sessions, revisionByFilePath);
|
|
2646
|
+
const summary = aggregateStatsSummary(reconciledRecords, normalizedScope, rates);
|
|
2647
|
+
this.dataCache.set(cacheKey, summary);
|
|
2648
|
+
return summary;
|
|
2649
|
+
}
|
|
1986
2650
|
getConfig() {
|
|
1987
2651
|
return this.configManager.getConfig();
|
|
1988
2652
|
}
|
|
@@ -1999,6 +2663,75 @@ class CodexServiceContext {
|
|
|
1999
2663
|
value
|
|
2000
2664
|
);
|
|
2001
2665
|
}
|
|
2666
|
+
async reconcileStatsSnapshot(currentSessions, revisionByFilePath) {
|
|
2667
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2668
|
+
const existingRecords = await this.statsSnapshotStore.getSessions();
|
|
2669
|
+
const existingBySessionId = new Map(existingRecords.map((record) => [record.sessionId, record]));
|
|
2670
|
+
const nextRecords = [];
|
|
2671
|
+
const activeSessionIds = /* @__PURE__ */ new Set();
|
|
2672
|
+
for (const session of currentSessions) {
|
|
2673
|
+
activeSessionIds.add(session.id);
|
|
2674
|
+
const revision = revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION;
|
|
2675
|
+
const existing = existingBySessionId.get(session.id);
|
|
2676
|
+
if (existing && existing.revision === revision && existing.tokenComputationVersion === TOKEN_COMPUTATION_VERSION) {
|
|
2677
|
+
nextRecords.push({
|
|
2678
|
+
...existing,
|
|
2679
|
+
archived: false,
|
|
2680
|
+
revision,
|
|
2681
|
+
filePath: session.filePath,
|
|
2682
|
+
cwd: session.cwd,
|
|
2683
|
+
startTime: session.startTime,
|
|
2684
|
+
modelUsages: session.modelUsages.length > 0 ? session.modelUsages : existing.modelUsages,
|
|
2685
|
+
lastSeenAt: nowIso
|
|
2686
|
+
});
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2689
|
+
let detail = null;
|
|
2690
|
+
try {
|
|
2691
|
+
detail = await this.getSessionDetail(session.id);
|
|
2692
|
+
} catch {
|
|
2693
|
+
detail = null;
|
|
2694
|
+
}
|
|
2695
|
+
if (!detail) {
|
|
2696
|
+
if (existing) {
|
|
2697
|
+
nextRecords.push({
|
|
2698
|
+
...existing,
|
|
2699
|
+
archived: false,
|
|
2700
|
+
revision,
|
|
2701
|
+
filePath: session.filePath,
|
|
2702
|
+
cwd: session.cwd,
|
|
2703
|
+
startTime: session.startTime,
|
|
2704
|
+
lastSeenAt: nowIso
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
const nextRecord = buildSessionStatsRecord(detail, revision, nowIso);
|
|
2710
|
+
nextRecord.archived = false;
|
|
2711
|
+
nextRecords.push(nextRecord);
|
|
2712
|
+
}
|
|
2713
|
+
for (const record of existingRecords) {
|
|
2714
|
+
if (activeSessionIds.has(record.sessionId)) {
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
nextRecords.push({
|
|
2718
|
+
...record,
|
|
2719
|
+
archived: true
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
nextRecords.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
2723
|
+
await this.statsSnapshotStore.saveSessions(nextRecords);
|
|
2724
|
+
return nextRecords;
|
|
2725
|
+
}
|
|
2726
|
+
async getSessionRevisionLookup(sessions) {
|
|
2727
|
+
const revisions = await Promise.all(
|
|
2728
|
+
sessions.map(async (session) => {
|
|
2729
|
+
const revision = await this.getSessionFileRevision(session.filePath);
|
|
2730
|
+
return [session.filePath, revision];
|
|
2731
|
+
})
|
|
2732
|
+
);
|
|
2733
|
+
return new Map(revisions);
|
|
2734
|
+
}
|
|
2002
2735
|
async findSessionById(sessionId) {
|
|
2003
2736
|
const sessions = await this.getAllSessions();
|
|
2004
2737
|
const match = sessions.find((session) => session.id === sessionId);
|