codex-devtools 0.1.10 → 0.2.0
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/bin/codex-devtools.cjs +1 -0
- package/dist-electron/main/chunks/{CodexServiceContext-CRkTP14W.cjs → CodexServiceContext-BYe2UXME.cjs} +788 -56
- package/dist-electron/main/index.cjs +46 -2
- package/dist-electron/main/standalone.cjs +76 -15
- package/dist-electron/preload/index.cjs +2 -0
- package/out/renderer/assets/{index-C1DUQHyp.js → index-C-iGxog-.js} +467 -23
- package/out/renderer/assets/{index-BTmVA30y.css → index-D3FYKy1U.css} +230 -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,24 @@ 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 hasNegative = delta.input_tokens < 0 || delta.cached_input_tokens < 0 || delta.output_tokens < 0 || delta.reasoning_output_tokens < 0 || delta.total_tokens < 0;
|
|
373
|
+
if (hasNegative) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const isDuplicate = delta.input_tokens === 0 && delta.cached_input_tokens === 0 && delta.output_tokens === 0 && delta.reasoning_output_tokens === 0 && delta.total_tokens === 0;
|
|
377
|
+
return isDuplicate ? null : delta;
|
|
378
|
+
}
|
|
379
|
+
function isSameTokenUsage(left, right) {
|
|
380
|
+
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;
|
|
381
|
+
}
|
|
357
382
|
const AGENTS_HEADING_PATTERN = /^#?\s*AGENTS\.md instructions\b/i;
|
|
358
383
|
const AGENTS_INSTRUCTIONS_BLOCK_PATTERN = /<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>/i;
|
|
359
384
|
const ENVIRONMENT_CONTEXT_WRAPPER_PATTERN = /^<environment_context>[\s\S]*<\/environment_context>$/i;
|
|
@@ -737,10 +762,10 @@ function pickPreferredEquivalentUserContent(pending, incoming) {
|
|
|
737
762
|
}
|
|
738
763
|
return pending;
|
|
739
764
|
}
|
|
740
|
-
function normalizeModel$
|
|
765
|
+
function normalizeModel$3(model) {
|
|
741
766
|
return model?.trim() ?? "";
|
|
742
767
|
}
|
|
743
|
-
function normalizeReasoningEffort$
|
|
768
|
+
function normalizeReasoningEffort$3(effort) {
|
|
744
769
|
const value = effort?.trim();
|
|
745
770
|
return value ? value : "unknown";
|
|
746
771
|
}
|
|
@@ -1077,11 +1102,11 @@ class CodexChunkBuilder {
|
|
|
1077
1102
|
continue;
|
|
1078
1103
|
}
|
|
1079
1104
|
if (isTurnContextEntry(entry)) {
|
|
1080
|
-
const model = normalizeModel$
|
|
1105
|
+
const model = normalizeModel$3(entry.payload.model);
|
|
1081
1106
|
if (model) {
|
|
1082
1107
|
const usage = {
|
|
1083
1108
|
model,
|
|
1084
|
-
reasoningEffort: normalizeReasoningEffort$
|
|
1109
|
+
reasoningEffort: normalizeReasoningEffort$3(entry.payload.effort)
|
|
1085
1110
|
};
|
|
1086
1111
|
if (lastSeenModelUsage === null) {
|
|
1087
1112
|
lastSeenModelUsage = usage;
|
|
@@ -1296,7 +1321,395 @@ class CodexChunkBuilder {
|
|
|
1296
1321
|
ai.pendingUsageToolIndex = null;
|
|
1297
1322
|
}
|
|
1298
1323
|
}
|
|
1299
|
-
|
|
1324
|
+
function normalizeRateKey(model) {
|
|
1325
|
+
return model.trim().toLowerCase();
|
|
1326
|
+
}
|
|
1327
|
+
function createZeroTokenTotals() {
|
|
1328
|
+
return {
|
|
1329
|
+
totalTokens: 0,
|
|
1330
|
+
inputTokens: 0,
|
|
1331
|
+
outputTokens: 0,
|
|
1332
|
+
cachedTokens: 0,
|
|
1333
|
+
reasoningTokens: 0
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function addTokenTotals(target, source) {
|
|
1337
|
+
target.totalTokens += source.totalTokens;
|
|
1338
|
+
target.inputTokens += source.inputTokens;
|
|
1339
|
+
target.outputTokens += source.outputTokens;
|
|
1340
|
+
target.cachedTokens += source.cachedTokens;
|
|
1341
|
+
target.reasoningTokens += source.reasoningTokens;
|
|
1342
|
+
}
|
|
1343
|
+
function tokenUsageToTotals(usage) {
|
|
1344
|
+
return {
|
|
1345
|
+
totalTokens: usage.total_tokens,
|
|
1346
|
+
inputTokens: usage.input_tokens,
|
|
1347
|
+
outputTokens: usage.output_tokens,
|
|
1348
|
+
cachedTokens: usage.cached_input_tokens,
|
|
1349
|
+
reasoningTokens: usage.reasoning_output_tokens
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function parseTimestamp(timestamp) {
|
|
1353
|
+
const value = new Date(timestamp);
|
|
1354
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
1355
|
+
}
|
|
1356
|
+
function localDateKey(date) {
|
|
1357
|
+
const year = date.getFullYear();
|
|
1358
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1359
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1360
|
+
return `${year}-${month}-${day}`;
|
|
1361
|
+
}
|
|
1362
|
+
function localHour(date) {
|
|
1363
|
+
return date.getHours();
|
|
1364
|
+
}
|
|
1365
|
+
function ensureDailyBucket(buckets, date) {
|
|
1366
|
+
const existing = buckets.get(date);
|
|
1367
|
+
if (existing) {
|
|
1368
|
+
return existing;
|
|
1369
|
+
}
|
|
1370
|
+
const next = {
|
|
1371
|
+
date,
|
|
1372
|
+
eventCount: 0,
|
|
1373
|
+
...createZeroTokenTotals()
|
|
1374
|
+
};
|
|
1375
|
+
buckets.set(date, next);
|
|
1376
|
+
return next;
|
|
1377
|
+
}
|
|
1378
|
+
function ensureHourlyBucket(buckets, hour) {
|
|
1379
|
+
const existing = buckets.get(hour);
|
|
1380
|
+
if (existing) {
|
|
1381
|
+
return existing;
|
|
1382
|
+
}
|
|
1383
|
+
const next = {
|
|
1384
|
+
hour,
|
|
1385
|
+
eventCount: 0,
|
|
1386
|
+
...createZeroTokenTotals()
|
|
1387
|
+
};
|
|
1388
|
+
buckets.set(hour, next);
|
|
1389
|
+
return next;
|
|
1390
|
+
}
|
|
1391
|
+
function ensureModelBucket(buckets, model, reasoningEffort) {
|
|
1392
|
+
const key = `${normalizeRateKey(model)}::${normalizeReasoningEffort$2(reasoningEffort)}`;
|
|
1393
|
+
const existing = buckets.get(key);
|
|
1394
|
+
if (existing) {
|
|
1395
|
+
return existing;
|
|
1396
|
+
}
|
|
1397
|
+
const next = {
|
|
1398
|
+
model,
|
|
1399
|
+
reasoningEffort,
|
|
1400
|
+
...createZeroTokenTotals()
|
|
1401
|
+
};
|
|
1402
|
+
buckets.set(key, next);
|
|
1403
|
+
return next;
|
|
1404
|
+
}
|
|
1405
|
+
function estimateUsageCostUsd(tokens, rate) {
|
|
1406
|
+
const nonReasoningOutput = Math.max(tokens.outputTokens - tokens.reasoningTokens, 0);
|
|
1407
|
+
const outputCost = nonReasoningOutput / 1e6 * rate.outputUsdPer1M;
|
|
1408
|
+
const reasoningCost = tokens.reasoningTokens / 1e6 * rate.reasoningOutputUsdPer1M;
|
|
1409
|
+
return tokens.inputTokens / 1e6 * rate.inputUsdPer1M + tokens.cachedTokens / 1e6 * rate.cachedInputUsdPer1M + outputCost + reasoningCost;
|
|
1410
|
+
}
|
|
1411
|
+
function buildRateLookup(rateCard) {
|
|
1412
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
1413
|
+
for (const rate of rateCard.models) {
|
|
1414
|
+
lookup.set(normalizeRateKey(rate.model), rate);
|
|
1415
|
+
}
|
|
1416
|
+
return lookup;
|
|
1417
|
+
}
|
|
1418
|
+
function buildHourlyResult(hourly) {
|
|
1419
|
+
const result = [];
|
|
1420
|
+
for (let hour = 0; hour <= 23; hour += 1) {
|
|
1421
|
+
const current = hourly.get(hour);
|
|
1422
|
+
if (!current) {
|
|
1423
|
+
result.push({
|
|
1424
|
+
hour,
|
|
1425
|
+
eventCount: 0,
|
|
1426
|
+
sessionCount: 0,
|
|
1427
|
+
...createZeroTokenTotals()
|
|
1428
|
+
});
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
result.push({
|
|
1432
|
+
hour,
|
|
1433
|
+
eventCount: current.eventCount,
|
|
1434
|
+
sessionCount: current.sessions.size,
|
|
1435
|
+
totalTokens: current.totalTokens,
|
|
1436
|
+
inputTokens: current.inputTokens,
|
|
1437
|
+
outputTokens: current.outputTokens,
|
|
1438
|
+
cachedTokens: current.cachedTokens,
|
|
1439
|
+
reasoningTokens: current.reasoningTokens
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
return result;
|
|
1443
|
+
}
|
|
1444
|
+
function normalizeModel$2(model) {
|
|
1445
|
+
const trimmed = model?.trim();
|
|
1446
|
+
return trimmed && trimmed.length > 0 ? trimmed : "unknown-model";
|
|
1447
|
+
}
|
|
1448
|
+
function normalizeReasoningEffort$2(reasoningEffort) {
|
|
1449
|
+
const trimmed = reasoningEffort?.trim();
|
|
1450
|
+
return trimmed && trimmed.length > 0 ? trimmed : "unknown";
|
|
1451
|
+
}
|
|
1452
|
+
function buildSessionStatsRecord(parsed, revision, nowIso) {
|
|
1453
|
+
const dailyBuckets = /* @__PURE__ */ new Map();
|
|
1454
|
+
const hourlyBuckets = /* @__PURE__ */ new Map();
|
|
1455
|
+
const modelBuckets = /* @__PURE__ */ new Map();
|
|
1456
|
+
const tokens = createZeroTokenTotals();
|
|
1457
|
+
let eventCount = 0;
|
|
1458
|
+
let currentModel = normalizeModel$2(parsed.session.model || parsed.sessionMeta?.payload.model);
|
|
1459
|
+
let currentReasoningEffort = "unknown";
|
|
1460
|
+
let previousTotalUsage = null;
|
|
1461
|
+
for (const modelUsage of parsed.session.modelUsages) {
|
|
1462
|
+
if (modelUsage.model) {
|
|
1463
|
+
currentModel = normalizeModel$2(modelUsage.model);
|
|
1464
|
+
currentReasoningEffort = normalizeReasoningEffort$2(modelUsage.reasoningEffort);
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const entry of parsed.entries) {
|
|
1469
|
+
if (isTurnContextEntry(entry)) {
|
|
1470
|
+
if (entry.payload.model) {
|
|
1471
|
+
currentModel = normalizeModel$2(entry.payload.model);
|
|
1472
|
+
}
|
|
1473
|
+
currentReasoningEffort = normalizeReasoningEffort$2(entry.payload.effort);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (!isEventMsgEntry(entry) || !isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const timestamp = parseTimestamp(entry.timestamp);
|
|
1480
|
+
const currentTotal = entry.payload.info.total_token_usage;
|
|
1481
|
+
let usage;
|
|
1482
|
+
if (previousTotalUsage) {
|
|
1483
|
+
const delta = diffTokenUsage(previousTotalUsage, currentTotal);
|
|
1484
|
+
if (delta) {
|
|
1485
|
+
usage = delta;
|
|
1486
|
+
} else if (isSameTokenUsage(previousTotalUsage, currentTotal)) {
|
|
1487
|
+
previousTotalUsage = currentTotal;
|
|
1488
|
+
continue;
|
|
1489
|
+
} else {
|
|
1490
|
+
usage = entry.payload.info.last_token_usage;
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
usage = currentTotal;
|
|
1494
|
+
}
|
|
1495
|
+
previousTotalUsage = currentTotal;
|
|
1496
|
+
const usageTotals = tokenUsageToTotals(usage);
|
|
1497
|
+
addTokenTotals(tokens, usageTotals);
|
|
1498
|
+
const modelTotals = ensureModelBucket(modelBuckets, currentModel, currentReasoningEffort);
|
|
1499
|
+
addTokenTotals(modelTotals, usageTotals);
|
|
1500
|
+
if (!timestamp) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
|
|
1504
|
+
addTokenTotals(dateBucket, usageTotals);
|
|
1505
|
+
const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
|
|
1506
|
+
addTokenTotals(hourBucket, usageTotals);
|
|
1507
|
+
}
|
|
1508
|
+
for (const classifiedEntry of parsed.classifiedEntries) {
|
|
1509
|
+
const timestamp = parseTimestamp(classifiedEntry.entry.timestamp);
|
|
1510
|
+
if (!timestamp) {
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
eventCount += 1;
|
|
1514
|
+
const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
|
|
1515
|
+
dateBucket.eventCount += 1;
|
|
1516
|
+
const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
|
|
1517
|
+
hourBucket.eventCount += 1;
|
|
1518
|
+
}
|
|
1519
|
+
const lastActivity = parsed.entries[parsed.entries.length - 1]?.timestamp ?? parsed.session.startTime;
|
|
1520
|
+
return {
|
|
1521
|
+
tokenComputationVersion: 2,
|
|
1522
|
+
sessionId: parsed.session.id,
|
|
1523
|
+
filePath: parsed.session.filePath,
|
|
1524
|
+
revision,
|
|
1525
|
+
archived: false,
|
|
1526
|
+
cwd: parsed.session.cwd,
|
|
1527
|
+
startTime: parsed.session.startTime,
|
|
1528
|
+
lastActivity,
|
|
1529
|
+
modelUsages: parsed.session.modelUsages,
|
|
1530
|
+
eventCount,
|
|
1531
|
+
turnCount: parsed.metrics.turnCount,
|
|
1532
|
+
toolCallCount: parsed.metrics.toolCallCount,
|
|
1533
|
+
durationMs: parsed.metrics.duration,
|
|
1534
|
+
tokens,
|
|
1535
|
+
modelTokenTotals: Array.from(modelBuckets.values()).sort((a, b) => b.totalTokens - a.totalTokens),
|
|
1536
|
+
dailyBuckets: Array.from(dailyBuckets.values()).sort((a, b) => a.date.localeCompare(b.date)),
|
|
1537
|
+
hourlyBuckets: Array.from(hourlyBuckets.values()).sort((a, b) => a.hour - b.hour),
|
|
1538
|
+
lastSeenAt: nowIso
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function aggregateStatsSummary(records, scope, rateCard) {
|
|
1542
|
+
const scopedRecords = records.filter((record) => {
|
|
1543
|
+
if (scope.type === "all") {
|
|
1544
|
+
return true;
|
|
1545
|
+
}
|
|
1546
|
+
return record.cwd === scope.cwd;
|
|
1547
|
+
});
|
|
1548
|
+
const daily = /* @__PURE__ */ new Map();
|
|
1549
|
+
const hourly = /* @__PURE__ */ new Map();
|
|
1550
|
+
const modelBreakdown = /* @__PURE__ */ new Map();
|
|
1551
|
+
const rateLookup = buildRateLookup(rateCard);
|
|
1552
|
+
const unpricedModels = /* @__PURE__ */ new Set();
|
|
1553
|
+
const totals = {
|
|
1554
|
+
sessions: 0,
|
|
1555
|
+
archivedSessions: 0,
|
|
1556
|
+
eventCount: 0,
|
|
1557
|
+
durationMs: 0,
|
|
1558
|
+
estimatedCostUsd: 0,
|
|
1559
|
+
...createZeroTokenTotals()
|
|
1560
|
+
};
|
|
1561
|
+
const costCoverage = {
|
|
1562
|
+
pricedTokens: 0,
|
|
1563
|
+
unpricedTokens: 0
|
|
1564
|
+
};
|
|
1565
|
+
for (const record of scopedRecords) {
|
|
1566
|
+
totals.sessions += 1;
|
|
1567
|
+
if (record.archived) {
|
|
1568
|
+
totals.archivedSessions += 1;
|
|
1569
|
+
}
|
|
1570
|
+
totals.eventCount += record.eventCount;
|
|
1571
|
+
totals.durationMs += record.durationMs;
|
|
1572
|
+
addTokenTotals(totals, record.tokens);
|
|
1573
|
+
for (const bucket of record.dailyBuckets) {
|
|
1574
|
+
const existing = daily.get(bucket.date) ?? {
|
|
1575
|
+
date: bucket.date,
|
|
1576
|
+
eventCount: 0,
|
|
1577
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1578
|
+
...createZeroTokenTotals()
|
|
1579
|
+
};
|
|
1580
|
+
existing.eventCount += bucket.eventCount;
|
|
1581
|
+
addTokenTotals(existing, bucket);
|
|
1582
|
+
existing.sessions.add(record.sessionId);
|
|
1583
|
+
daily.set(bucket.date, existing);
|
|
1584
|
+
}
|
|
1585
|
+
for (const bucket of record.hourlyBuckets) {
|
|
1586
|
+
const existing = hourly.get(bucket.hour) ?? {
|
|
1587
|
+
hour: bucket.hour,
|
|
1588
|
+
eventCount: 0,
|
|
1589
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1590
|
+
...createZeroTokenTotals()
|
|
1591
|
+
};
|
|
1592
|
+
existing.eventCount += bucket.eventCount;
|
|
1593
|
+
addTokenTotals(existing, bucket);
|
|
1594
|
+
existing.sessions.add(record.sessionId);
|
|
1595
|
+
hourly.set(bucket.hour, existing);
|
|
1596
|
+
}
|
|
1597
|
+
for (const modelTotals of record.modelTokenTotals) {
|
|
1598
|
+
const rate = rateLookup.get(normalizeRateKey(modelTotals.model));
|
|
1599
|
+
const modelKey = `${normalizeRateKey(modelTotals.model)}::${normalizeReasoningEffort$2(modelTotals.reasoningEffort)}`;
|
|
1600
|
+
const existing = modelBreakdown.get(modelKey) ?? {
|
|
1601
|
+
model: modelTotals.model,
|
|
1602
|
+
reasoningEffort: modelTotals.reasoningEffort,
|
|
1603
|
+
estimatedCostUsd: 0,
|
|
1604
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1605
|
+
archivedSessions: /* @__PURE__ */ new Set(),
|
|
1606
|
+
...createZeroTokenTotals()
|
|
1607
|
+
};
|
|
1608
|
+
addTokenTotals(existing, modelTotals);
|
|
1609
|
+
existing.sessions.add(record.sessionId);
|
|
1610
|
+
if (record.archived) {
|
|
1611
|
+
existing.archivedSessions.add(record.sessionId);
|
|
1612
|
+
}
|
|
1613
|
+
if (rate) {
|
|
1614
|
+
const usageCost = estimateUsageCostUsd(modelTotals, rate);
|
|
1615
|
+
existing.estimatedCostUsd += usageCost;
|
|
1616
|
+
totals.estimatedCostUsd += usageCost;
|
|
1617
|
+
costCoverage.pricedTokens += modelTotals.totalTokens;
|
|
1618
|
+
} else {
|
|
1619
|
+
costCoverage.unpricedTokens += modelTotals.totalTokens;
|
|
1620
|
+
unpricedModels.add(modelTotals.model);
|
|
1621
|
+
}
|
|
1622
|
+
modelBreakdown.set(modelKey, existing);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const dailyPoints = Array.from(daily.values()).map((bucket) => ({
|
|
1626
|
+
date: bucket.date,
|
|
1627
|
+
eventCount: bucket.eventCount,
|
|
1628
|
+
sessionCount: bucket.sessions.size,
|
|
1629
|
+
totalTokens: bucket.totalTokens,
|
|
1630
|
+
inputTokens: bucket.inputTokens,
|
|
1631
|
+
outputTokens: bucket.outputTokens,
|
|
1632
|
+
cachedTokens: bucket.cachedTokens,
|
|
1633
|
+
reasoningTokens: bucket.reasoningTokens
|
|
1634
|
+
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
1635
|
+
const hourlyPoints = buildHourlyResult(hourly);
|
|
1636
|
+
const models = Array.from(modelBreakdown.values()).map((model) => ({
|
|
1637
|
+
model: model.model,
|
|
1638
|
+
reasoningEffort: model.reasoningEffort,
|
|
1639
|
+
sessionCount: model.sessions.size,
|
|
1640
|
+
archivedSessionCount: model.archivedSessions.size,
|
|
1641
|
+
estimatedCostUsd: Number(model.estimatedCostUsd.toFixed(6)),
|
|
1642
|
+
totalTokens: model.totalTokens,
|
|
1643
|
+
inputTokens: model.inputTokens,
|
|
1644
|
+
outputTokens: model.outputTokens,
|
|
1645
|
+
cachedTokens: model.cachedTokens,
|
|
1646
|
+
reasoningTokens: model.reasoningTokens
|
|
1647
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1648
|
+
const reasoningMap = /* @__PURE__ */ new Map();
|
|
1649
|
+
for (const model of modelBreakdown.values()) {
|
|
1650
|
+
const existing = reasoningMap.get(model.reasoningEffort) ?? {
|
|
1651
|
+
reasoningEffort: model.reasoningEffort,
|
|
1652
|
+
estimatedCostUsd: 0,
|
|
1653
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
1654
|
+
...createZeroTokenTotals()
|
|
1655
|
+
};
|
|
1656
|
+
for (const sessionId of model.sessions) {
|
|
1657
|
+
existing.sessions.add(sessionId);
|
|
1658
|
+
}
|
|
1659
|
+
existing.estimatedCostUsd += model.estimatedCostUsd;
|
|
1660
|
+
addTokenTotals(existing, model);
|
|
1661
|
+
reasoningMap.set(model.reasoningEffort, existing);
|
|
1662
|
+
}
|
|
1663
|
+
const reasoningEfforts = Array.from(reasoningMap.values()).map((reasoning) => ({
|
|
1664
|
+
reasoningEffort: reasoning.reasoningEffort,
|
|
1665
|
+
sessionCount: reasoning.sessions.size,
|
|
1666
|
+
estimatedCostUsd: Number(reasoning.estimatedCostUsd.toFixed(6)),
|
|
1667
|
+
totalTokens: reasoning.totalTokens,
|
|
1668
|
+
inputTokens: reasoning.inputTokens,
|
|
1669
|
+
outputTokens: reasoning.outputTokens,
|
|
1670
|
+
cachedTokens: reasoning.cachedTokens,
|
|
1671
|
+
reasoningTokens: reasoning.reasoningTokens
|
|
1672
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1673
|
+
const topDays = [...dailyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((day) => ({
|
|
1674
|
+
date: day.date,
|
|
1675
|
+
eventCount: day.eventCount,
|
|
1676
|
+
sessionCount: day.sessionCount,
|
|
1677
|
+
totalTokens: day.totalTokens,
|
|
1678
|
+
outputTokens: day.outputTokens
|
|
1679
|
+
}));
|
|
1680
|
+
const topHours = [...hourlyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((hour) => ({
|
|
1681
|
+
hour: hour.hour,
|
|
1682
|
+
eventCount: hour.eventCount,
|
|
1683
|
+
sessionCount: hour.sessionCount,
|
|
1684
|
+
totalTokens: hour.totalTokens,
|
|
1685
|
+
outputTokens: hour.outputTokens
|
|
1686
|
+
}));
|
|
1687
|
+
return {
|
|
1688
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1689
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "local",
|
|
1690
|
+
scope,
|
|
1691
|
+
totals: {
|
|
1692
|
+
...totals,
|
|
1693
|
+
estimatedCostUsd: Number(totals.estimatedCostUsd.toFixed(6))
|
|
1694
|
+
},
|
|
1695
|
+
daily: dailyPoints,
|
|
1696
|
+
hourly: hourlyPoints,
|
|
1697
|
+
topDays,
|
|
1698
|
+
topHours,
|
|
1699
|
+
models,
|
|
1700
|
+
reasoningEfforts,
|
|
1701
|
+
costCoverage: {
|
|
1702
|
+
pricedTokens: costCoverage.pricedTokens,
|
|
1703
|
+
unpricedTokens: costCoverage.unpricedTokens,
|
|
1704
|
+
unpricedModels: Array.from(unpricedModels.values()).sort((a, b) => a.localeCompare(b))
|
|
1705
|
+
},
|
|
1706
|
+
rates: {
|
|
1707
|
+
updatedAt: rateCard.updatedAt,
|
|
1708
|
+
source: rateCard.source
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
const logger$4 = createLogger("Main:jsonl");
|
|
1300
1713
|
const defaultParser = (value) => value;
|
|
1301
1714
|
function waitForStreamClose(stream) {
|
|
1302
1715
|
if (stream.closed) {
|
|
@@ -1343,7 +1756,7 @@ async function streamJsonlFile(filePath, options) {
|
|
|
1343
1756
|
} catch (error) {
|
|
1344
1757
|
options.onError?.(error, line, lineNumber);
|
|
1345
1758
|
if (!options.onError) {
|
|
1346
|
-
logger$
|
|
1759
|
+
logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1347
1760
|
}
|
|
1348
1761
|
}
|
|
1349
1762
|
}
|
|
@@ -1384,7 +1797,7 @@ async function readFirstJsonlEntry(filePath, parser) {
|
|
|
1384
1797
|
break;
|
|
1385
1798
|
}
|
|
1386
1799
|
} catch (error) {
|
|
1387
|
-
logger$
|
|
1800
|
+
logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1388
1801
|
}
|
|
1389
1802
|
}
|
|
1390
1803
|
} finally {
|
|
@@ -1392,7 +1805,7 @@ async function readFirstJsonlEntry(filePath, parser) {
|
|
|
1392
1805
|
}
|
|
1393
1806
|
return firstEntry;
|
|
1394
1807
|
}
|
|
1395
|
-
const logger$
|
|
1808
|
+
const logger$3 = createLogger("Service:CodexSessionScanner");
|
|
1396
1809
|
function isWithinDateRange(timestamp, options) {
|
|
1397
1810
|
const current = new Date(timestamp).getTime();
|
|
1398
1811
|
if (Number.isNaN(current)) {
|
|
@@ -1438,7 +1851,7 @@ class CodexSessionScanner {
|
|
|
1438
1851
|
(value) => isSessionMetaEntry(value) ? value : null
|
|
1439
1852
|
);
|
|
1440
1853
|
if (!metaEntry) {
|
|
1441
|
-
logger$
|
|
1854
|
+
logger$3.warn(`Skipping session file without valid session_meta: ${filePath}`);
|
|
1442
1855
|
continue;
|
|
1443
1856
|
}
|
|
1444
1857
|
if (!isWithinDateRange(metaEntry.timestamp, options)) {
|
|
@@ -1575,6 +1988,7 @@ class CodexSessionParser {
|
|
|
1575
1988
|
let firstTimestamp = null;
|
|
1576
1989
|
let lastTimestamp = null;
|
|
1577
1990
|
let firstTurnContextModel = null;
|
|
1991
|
+
let previousTotalUsage = null;
|
|
1578
1992
|
const modelUsages = [];
|
|
1579
1993
|
const modelUsageKeys = /* @__PURE__ */ new Set();
|
|
1580
1994
|
for (const entry of entries) {
|
|
@@ -1615,7 +2029,22 @@ class CodexSessionParser {
|
|
|
1615
2029
|
if (isEventMsgEntry(entry)) {
|
|
1616
2030
|
eventMessages.push(entry);
|
|
1617
2031
|
if (isTokenCountPayload(entry.payload) && entry.payload.info) {
|
|
1618
|
-
const
|
|
2032
|
+
const currentTotal = entry.payload.info.total_token_usage;
|
|
2033
|
+
let usage;
|
|
2034
|
+
if (previousTotalUsage) {
|
|
2035
|
+
const delta = diffTokenUsage(previousTotalUsage, currentTotal);
|
|
2036
|
+
if (delta) {
|
|
2037
|
+
usage = delta;
|
|
2038
|
+
} else if (isSameTokenUsage(previousTotalUsage, currentTotal)) {
|
|
2039
|
+
previousTotalUsage = currentTotal;
|
|
2040
|
+
continue;
|
|
2041
|
+
} else {
|
|
2042
|
+
usage = entry.payload.info.last_token_usage;
|
|
2043
|
+
}
|
|
2044
|
+
} else {
|
|
2045
|
+
usage = currentTotal;
|
|
2046
|
+
}
|
|
2047
|
+
previousTotalUsage = currentTotal;
|
|
1619
2048
|
metrics.inputTokens += usage.input_tokens;
|
|
1620
2049
|
metrics.cachedTokens += usage.cached_input_tokens;
|
|
1621
2050
|
metrics.outputTokens += usage.output_tokens;
|
|
@@ -1713,7 +2142,7 @@ class DataCache {
|
|
|
1713
2142
|
return `${cwdHash}/${sessionId}`;
|
|
1714
2143
|
}
|
|
1715
2144
|
}
|
|
1716
|
-
const logger = createLogger("Service:FileWatcher");
|
|
2145
|
+
const logger$2 = createLogger("Service:FileWatcher");
|
|
1717
2146
|
const DEBOUNCE_MS = 100;
|
|
1718
2147
|
class FileWatcher extends node_events.EventEmitter {
|
|
1719
2148
|
constructor(sessionsPath = path__namespace.join(os__namespace.homedir(), ".codex", "sessions")) {
|
|
@@ -1739,7 +2168,7 @@ class FileWatcher extends node_events.EventEmitter {
|
|
|
1739
2168
|
}
|
|
1740
2169
|
);
|
|
1741
2170
|
} catch (error) {
|
|
1742
|
-
logger.error(`Failed to start watcher for ${this.sessionsPath}`, error);
|
|
2171
|
+
logger$2.error(`Failed to start watcher for ${this.sessionsPath}`, error);
|
|
1743
2172
|
}
|
|
1744
2173
|
}
|
|
1745
2174
|
stop() {
|
|
@@ -1787,10 +2216,179 @@ class FileWatcher extends node_events.EventEmitter {
|
|
|
1787
2216
|
return fs__namespace.existsSync(targetPath) ? "created" : "deleted";
|
|
1788
2217
|
}
|
|
1789
2218
|
}
|
|
2219
|
+
const logger$1 = createLogger("Service:ModelRatesStore");
|
|
2220
|
+
const RATES_VERSION = 1;
|
|
2221
|
+
const BUNDLED_DEFAULT_RATES = [
|
|
2222
|
+
{ model: "gpt-5.2", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
|
|
2223
|
+
{ model: "gpt-5.1", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2224
|
+
{ model: "gpt-5", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2225
|
+
{ model: "gpt-5-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
|
|
2226
|
+
{ model: "gpt-5-nano", inputUsdPer1M: 0.05, cachedInputUsdPer1M: 5e-3, outputUsdPer1M: 0.4, reasoningOutputUsdPer1M: 0.4 },
|
|
2227
|
+
{ model: "gpt-5.2-codex", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
|
|
2228
|
+
{ model: "gpt-5.1-codex-max", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2229
|
+
{ model: "gpt-5.1-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2230
|
+
{ model: "gpt-5-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
|
|
2231
|
+
{ model: "gpt-5.1-codex-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
|
|
2232
|
+
{ model: "codex-mini-latest", inputUsdPer1M: 1.5, cachedInputUsdPer1M: 0.375, outputUsdPer1M: 6, reasoningOutputUsdPer1M: 6 }
|
|
2233
|
+
];
|
|
2234
|
+
const DEFAULT_RATE_CARD = {
|
|
2235
|
+
updatedAt: null,
|
|
2236
|
+
source: "bundled-defaults",
|
|
2237
|
+
models: BUNDLED_DEFAULT_RATES,
|
|
2238
|
+
warnings: ["Bundled rates may be outdated. Pricing refresh is currently disabled."]
|
|
2239
|
+
};
|
|
2240
|
+
function isRecord$1(value) {
|
|
2241
|
+
return typeof value === "object" && value !== null;
|
|
2242
|
+
}
|
|
2243
|
+
function isString$1(value) {
|
|
2244
|
+
return typeof value === "string";
|
|
2245
|
+
}
|
|
2246
|
+
function isFiniteNumber$1(value) {
|
|
2247
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
2248
|
+
}
|
|
2249
|
+
function isCodexModelRate(value) {
|
|
2250
|
+
return isRecord$1(value) && isString$1(value.model) && isFiniteNumber$1(value.inputUsdPer1M) && isFiniteNumber$1(value.cachedInputUsdPer1M) && isFiniteNumber$1(value.outputUsdPer1M) && isFiniteNumber$1(value.reasoningOutputUsdPer1M);
|
|
2251
|
+
}
|
|
2252
|
+
function isRatesFile(value) {
|
|
2253
|
+
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);
|
|
2254
|
+
}
|
|
2255
|
+
function cloneRateCard(card) {
|
|
2256
|
+
return structuredClone(card);
|
|
2257
|
+
}
|
|
2258
|
+
class ModelRatesStore {
|
|
2259
|
+
constructor(filePath) {
|
|
2260
|
+
this.filePath = filePath;
|
|
2261
|
+
}
|
|
2262
|
+
async getRateCard() {
|
|
2263
|
+
const file = await this.readRateFile();
|
|
2264
|
+
return {
|
|
2265
|
+
updatedAt: file.updatedAt,
|
|
2266
|
+
source: file.source,
|
|
2267
|
+
models: file.models,
|
|
2268
|
+
warnings: file.warnings
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
async refreshFromPricingPage() {
|
|
2272
|
+
const current = await this.getRateCard();
|
|
2273
|
+
return {
|
|
2274
|
+
...current,
|
|
2275
|
+
warnings: [...current.warnings, "Pricing refresh is disabled in this build."],
|
|
2276
|
+
refreshed: false
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
async readRateFile() {
|
|
2280
|
+
try {
|
|
2281
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
2282
|
+
const parsed = JSON.parse(raw);
|
|
2283
|
+
if (!isRatesFile(parsed) || parsed.version !== RATES_VERSION) {
|
|
2284
|
+
return {
|
|
2285
|
+
version: RATES_VERSION,
|
|
2286
|
+
updatedAt: DEFAULT_RATE_CARD.updatedAt,
|
|
2287
|
+
source: DEFAULT_RATE_CARD.source,
|
|
2288
|
+
models: cloneRateCard(DEFAULT_RATE_CARD).models,
|
|
2289
|
+
warnings: [...DEFAULT_RATE_CARD.warnings]
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
return parsed;
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
if (error.code !== "ENOENT") {
|
|
2295
|
+
logger$1.warn("Failed to load model rates, falling back to bundled defaults", error);
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
version: RATES_VERSION,
|
|
2299
|
+
updatedAt: DEFAULT_RATE_CARD.updatedAt,
|
|
2300
|
+
source: DEFAULT_RATE_CARD.source,
|
|
2301
|
+
models: cloneRateCard(DEFAULT_RATE_CARD).models,
|
|
2302
|
+
warnings: [...DEFAULT_RATE_CARD.warnings]
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
const logger = createLogger("Service:StatsSnapshotStore");
|
|
2308
|
+
const SNAPSHOT_VERSION = 1;
|
|
2309
|
+
function isRecord(value) {
|
|
2310
|
+
return typeof value === "object" && value !== null;
|
|
2311
|
+
}
|
|
2312
|
+
function isFiniteNumber(value) {
|
|
2313
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
2314
|
+
}
|
|
2315
|
+
function isString(value) {
|
|
2316
|
+
return typeof value === "string";
|
|
2317
|
+
}
|
|
2318
|
+
function isTokenTotals(value) {
|
|
2319
|
+
return isRecord(value) && isFiniteNumber(value.totalTokens) && isFiniteNumber(value.inputTokens) && isFiniteNumber(value.outputTokens) && isFiniteNumber(value.cachedTokens) && isFiniteNumber(value.reasoningTokens);
|
|
2320
|
+
}
|
|
2321
|
+
function isDailyBucket(value) {
|
|
2322
|
+
return isRecord(value) && isString(value.date) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
|
|
2323
|
+
}
|
|
2324
|
+
function isHourlyBucket(value) {
|
|
2325
|
+
return isRecord(value) && isFiniteNumber(value.hour) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
|
|
2326
|
+
}
|
|
2327
|
+
function isModelTotals(value) {
|
|
2328
|
+
return isRecord(value) && isString(value.model) && isString(value.reasoningEffort) && isTokenTotals(value);
|
|
2329
|
+
}
|
|
2330
|
+
function isSessionModelUsage(value) {
|
|
2331
|
+
return isRecord(value) && isString(value.model) && isString(value.reasoningEffort);
|
|
2332
|
+
}
|
|
2333
|
+
function isSessionRecord(value) {
|
|
2334
|
+
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);
|
|
2335
|
+
}
|
|
2336
|
+
function isSnapshotFile(value) {
|
|
2337
|
+
return isRecord(value) && isFiniteNumber(value.version) && Array.isArray(value.sessions) && value.sessions.every(isSessionRecord);
|
|
2338
|
+
}
|
|
2339
|
+
function cloneSessions(rows) {
|
|
2340
|
+
return structuredClone(rows);
|
|
2341
|
+
}
|
|
2342
|
+
class StatsSnapshotStore {
|
|
2343
|
+
constructor(filePath) {
|
|
2344
|
+
this.filePath = filePath;
|
|
2345
|
+
}
|
|
2346
|
+
async getSessions() {
|
|
2347
|
+
const snapshot = await this.readSnapshot();
|
|
2348
|
+
return cloneSessions(snapshot.sessions);
|
|
2349
|
+
}
|
|
2350
|
+
async saveSessions(rows) {
|
|
2351
|
+
const snapshot = {
|
|
2352
|
+
version: SNAPSHOT_VERSION,
|
|
2353
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2354
|
+
sessions: cloneSessions(rows)
|
|
2355
|
+
};
|
|
2356
|
+
const directory = path__namespace.dirname(this.filePath);
|
|
2357
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
2358
|
+
const tempPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2359
|
+
await fs.promises.writeFile(tempPath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2360
|
+
await fs.promises.rename(tempPath, this.filePath);
|
|
2361
|
+
}
|
|
2362
|
+
async readSnapshot() {
|
|
2363
|
+
try {
|
|
2364
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
2365
|
+
const parsed = JSON.parse(raw);
|
|
2366
|
+
if (!isSnapshotFile(parsed) || parsed.version !== SNAPSHOT_VERSION) {
|
|
2367
|
+
return {
|
|
2368
|
+
version: SNAPSHOT_VERSION,
|
|
2369
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2370
|
+
sessions: []
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
return parsed;
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
if (error.code !== "ENOENT") {
|
|
2376
|
+
logger.warn("Failed to read stats snapshot, using empty snapshot", error);
|
|
2377
|
+
}
|
|
2378
|
+
return {
|
|
2379
|
+
version: SNAPSHOT_VERSION,
|
|
2380
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2381
|
+
sessions: []
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
1790
2386
|
const DETAIL_CACHE_PREFIX = "detail-v2";
|
|
1791
2387
|
const CHUNKS_CACHE_PREFIX = "chunks-v2";
|
|
1792
2388
|
const SESSIONS_CACHE_PREFIX = "sessions";
|
|
2389
|
+
const STATS_CACHE_PREFIX = "stats-v1";
|
|
1793
2390
|
const UNKNOWN_REVISION = "unknown-revision";
|
|
2391
|
+
const TOKEN_COMPUTATION_VERSION = 2;
|
|
1794
2392
|
function hasCompactionSignals(entries) {
|
|
1795
2393
|
return entries.some(
|
|
1796
2394
|
(entry) => isCompactedEntry(entry) || isCompactionEntry(entry) || isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)
|
|
@@ -1857,15 +2455,53 @@ function buildProjectsFromSessions(sessions) {
|
|
|
1857
2455
|
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
1858
2456
|
);
|
|
1859
2457
|
}
|
|
2458
|
+
function normalizeStatsScope(scope) {
|
|
2459
|
+
if (!scope || scope.type === "all") {
|
|
2460
|
+
return { type: "all" };
|
|
2461
|
+
}
|
|
2462
|
+
const cwd = scope.cwd?.trim();
|
|
2463
|
+
if (!cwd) {
|
|
2464
|
+
return { type: "all" };
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
type: "project",
|
|
2468
|
+
cwd
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
function serializeStatsScope(scope) {
|
|
2472
|
+
if (scope.type === "all") {
|
|
2473
|
+
return "all";
|
|
2474
|
+
}
|
|
2475
|
+
return `project:${scope.cwd}`;
|
|
2476
|
+
}
|
|
2477
|
+
function buildStatsSessionSignature(sessions, revisionByFilePath) {
|
|
2478
|
+
const hash = node_crypto.createHash("sha1");
|
|
2479
|
+
const sortedSessions = [...sessions].sort(
|
|
2480
|
+
(left, right) => left.filePath.localeCompare(right.filePath) || left.id.localeCompare(right.id)
|
|
2481
|
+
);
|
|
2482
|
+
for (const session of sortedSessions) {
|
|
2483
|
+
hash.update(session.id);
|
|
2484
|
+
hash.update("\0");
|
|
2485
|
+
hash.update(session.filePath);
|
|
2486
|
+
hash.update("\0");
|
|
2487
|
+
hash.update(revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION);
|
|
2488
|
+
hash.update("\n");
|
|
2489
|
+
}
|
|
2490
|
+
return hash.digest("hex");
|
|
2491
|
+
}
|
|
1860
2492
|
class CodexServiceContext {
|
|
1861
2493
|
constructor(options = {}) {
|
|
1862
2494
|
const sessionsPath = options.sessionsPath ?? process.env.CODEX_SESSIONS_PATH;
|
|
2495
|
+
const configPath = options.configPath ?? DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH;
|
|
1863
2496
|
this.scanner = new CodexSessionScanner(sessionsPath);
|
|
1864
2497
|
this.parser = new CodexSessionParser();
|
|
1865
2498
|
this.chunkBuilder = new CodexChunkBuilder();
|
|
1866
2499
|
this.dataCache = new DataCache(options.cacheSize ?? 200, options.cacheTtlMinutes ?? 10);
|
|
1867
2500
|
this.watcher = new FileWatcher(sessionsPath);
|
|
1868
|
-
this.configManager = new ConfigManager(
|
|
2501
|
+
this.configManager = new ConfigManager(configPath);
|
|
2502
|
+
const metadataDirectory = path__namespace.dirname(configPath);
|
|
2503
|
+
this.statsSnapshotStore = new StatsSnapshotStore(path__namespace.join(metadataDirectory, "stats-snapshot.json"));
|
|
2504
|
+
this.modelRatesStore = new ModelRatesStore(path__namespace.join(metadataDirectory, "stats-rates.json"));
|
|
1869
2505
|
this.removeFileChangeListener = this.watcher.onFileChange(() => {
|
|
1870
2506
|
this.dataCache.clear();
|
|
1871
2507
|
});
|
|
@@ -1983,6 +2619,33 @@ class CodexServiceContext {
|
|
|
1983
2619
|
results
|
|
1984
2620
|
};
|
|
1985
2621
|
}
|
|
2622
|
+
async getModelRates() {
|
|
2623
|
+
return this.modelRatesStore.getRateCard();
|
|
2624
|
+
}
|
|
2625
|
+
async refreshModelRates() {
|
|
2626
|
+
const refreshed = await this.modelRatesStore.refreshFromPricingPage();
|
|
2627
|
+
this.dataCache.clear();
|
|
2628
|
+
return refreshed;
|
|
2629
|
+
}
|
|
2630
|
+
async getStats(scope = { type: "all" }) {
|
|
2631
|
+
const normalizedScope = normalizeStatsScope(scope);
|
|
2632
|
+
const rates = await this.getModelRates();
|
|
2633
|
+
const sessions = await this.scanner.scanSessions();
|
|
2634
|
+
const revisionByFilePath = await this.getSessionRevisionLookup(sessions);
|
|
2635
|
+
const sessionSignature = buildStatsSessionSignature(sessions, revisionByFilePath);
|
|
2636
|
+
const cacheKey = DataCache.buildKey(
|
|
2637
|
+
STATS_CACHE_PREFIX,
|
|
2638
|
+
`${serializeStatsScope(normalizedScope)}:${rates.updatedAt ?? "never"}:${sessionSignature}`
|
|
2639
|
+
);
|
|
2640
|
+
const cached = this.dataCache.get(cacheKey);
|
|
2641
|
+
if (cached) {
|
|
2642
|
+
return cached;
|
|
2643
|
+
}
|
|
2644
|
+
const reconciledRecords = await this.reconcileStatsSnapshot(sessions, revisionByFilePath);
|
|
2645
|
+
const summary = aggregateStatsSummary(reconciledRecords, normalizedScope, rates);
|
|
2646
|
+
this.dataCache.set(cacheKey, summary);
|
|
2647
|
+
return summary;
|
|
2648
|
+
}
|
|
1986
2649
|
getConfig() {
|
|
1987
2650
|
return this.configManager.getConfig();
|
|
1988
2651
|
}
|
|
@@ -1999,6 +2662,75 @@ class CodexServiceContext {
|
|
|
1999
2662
|
value
|
|
2000
2663
|
);
|
|
2001
2664
|
}
|
|
2665
|
+
async reconcileStatsSnapshot(currentSessions, revisionByFilePath) {
|
|
2666
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2667
|
+
const existingRecords = await this.statsSnapshotStore.getSessions();
|
|
2668
|
+
const existingBySessionId = new Map(existingRecords.map((record) => [record.sessionId, record]));
|
|
2669
|
+
const nextRecords = [];
|
|
2670
|
+
const activeSessionIds = /* @__PURE__ */ new Set();
|
|
2671
|
+
for (const session of currentSessions) {
|
|
2672
|
+
activeSessionIds.add(session.id);
|
|
2673
|
+
const revision = revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION;
|
|
2674
|
+
const existing = existingBySessionId.get(session.id);
|
|
2675
|
+
if (existing && existing.revision === revision && existing.tokenComputationVersion === TOKEN_COMPUTATION_VERSION) {
|
|
2676
|
+
nextRecords.push({
|
|
2677
|
+
...existing,
|
|
2678
|
+
archived: false,
|
|
2679
|
+
revision,
|
|
2680
|
+
filePath: session.filePath,
|
|
2681
|
+
cwd: session.cwd,
|
|
2682
|
+
startTime: session.startTime,
|
|
2683
|
+
modelUsages: session.modelUsages.length > 0 ? session.modelUsages : existing.modelUsages,
|
|
2684
|
+
lastSeenAt: nowIso
|
|
2685
|
+
});
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
let detail = null;
|
|
2689
|
+
try {
|
|
2690
|
+
detail = await this.getSessionDetail(session.id);
|
|
2691
|
+
} catch {
|
|
2692
|
+
detail = null;
|
|
2693
|
+
}
|
|
2694
|
+
if (!detail) {
|
|
2695
|
+
if (existing) {
|
|
2696
|
+
nextRecords.push({
|
|
2697
|
+
...existing,
|
|
2698
|
+
archived: false,
|
|
2699
|
+
revision,
|
|
2700
|
+
filePath: session.filePath,
|
|
2701
|
+
cwd: session.cwd,
|
|
2702
|
+
startTime: session.startTime,
|
|
2703
|
+
lastSeenAt: nowIso
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
const nextRecord = buildSessionStatsRecord(detail, revision, nowIso);
|
|
2709
|
+
nextRecord.archived = false;
|
|
2710
|
+
nextRecords.push(nextRecord);
|
|
2711
|
+
}
|
|
2712
|
+
for (const record of existingRecords) {
|
|
2713
|
+
if (activeSessionIds.has(record.sessionId)) {
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
nextRecords.push({
|
|
2717
|
+
...record,
|
|
2718
|
+
archived: true
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
nextRecords.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
2722
|
+
await this.statsSnapshotStore.saveSessions(nextRecords);
|
|
2723
|
+
return nextRecords;
|
|
2724
|
+
}
|
|
2725
|
+
async getSessionRevisionLookup(sessions) {
|
|
2726
|
+
const revisions = await Promise.all(
|
|
2727
|
+
sessions.map(async (session) => {
|
|
2728
|
+
const revision = await this.getSessionFileRevision(session.filePath);
|
|
2729
|
+
return [session.filePath, revision];
|
|
2730
|
+
})
|
|
2731
|
+
);
|
|
2732
|
+
return new Map(revisions);
|
|
2733
|
+
}
|
|
2002
2734
|
async findSessionById(sessionId) {
|
|
2003
2735
|
const sessions = await this.getAllSessions();
|
|
2004
2736
|
const match = sessions.find((session) => session.id === sessionId);
|