codexmeter 1.0.21 → 1.0.22
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/README.md +1 -1
- package/bin/codexmeter.js +19 -2
- package/dist/assets/index-CvHA7-49.js +31 -0
- package/dist/index.html +1 -1
- package/package.json +16 -16
- package/server/aggregator.js +34 -41
- package/server/day-key.js +44 -0
- package/server/live-state.js +47 -59
- package/server/rollout-reader.js +45 -30
- package/server/sqlite-reader.js +2 -5
- package/dist/assets/index-BfQCWE9K.js +0 -34
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CvHA7-49.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-DdbEQTfc.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codexmeter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Local telemetry dashboard for Codex CLI usage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"codexmeter": "bin/codexmeter.js"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": "
|
|
10
|
+
"node": ">=22.13.0"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
@@ -23,30 +23,30 @@
|
|
|
23
23
|
"prepack": "npm run build",
|
|
24
24
|
"pretest": "npm run build",
|
|
25
25
|
"test": "node --test \"test/*.test.js\" \"test/**/*.test.js\"",
|
|
26
|
+
"audit:prod": "npm audit --omit=dev --audit-level=moderate",
|
|
26
27
|
"start": "node bin/codexmeter.js",
|
|
27
28
|
"preview": "npm run build && node bin/codexmeter.js",
|
|
28
29
|
"prepublishOnly": "npm run build"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"express": "^5.1.0",
|
|
32
|
+
"commander": "^15.0.0",
|
|
33
|
+
"express": "^5.2.1",
|
|
34
34
|
"fast-glob": "^3.3.3",
|
|
35
|
-
"ffmpeg-static": "^5.
|
|
35
|
+
"ffmpeg-static": "^5.3.0",
|
|
36
36
|
"open": "^11.0.0",
|
|
37
|
-
"playwright-core": "^1.
|
|
37
|
+
"playwright-core": "^1.61.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@tanstack/react-table": "^8.21.3",
|
|
41
|
-
"@tanstack/react-virtual": "^3.
|
|
42
|
-
"@types/react": "^19.
|
|
43
|
-
"@types/react-dom": "^19.
|
|
44
|
-
"@vitejs/plugin-react": "^6.0.
|
|
45
|
-
"echarts": "^6.
|
|
46
|
-
"echarts-for-react": "^3.0.
|
|
41
|
+
"@tanstack/react-virtual": "^3.14.3",
|
|
42
|
+
"@types/react": "^19.2.17",
|
|
43
|
+
"@types/react-dom": "^19.2.3",
|
|
44
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
45
|
+
"echarts": "^6.1.0",
|
|
46
|
+
"echarts-for-react": "^3.0.6",
|
|
47
47
|
"nodemon": "^3.1.14",
|
|
48
|
-
"react": "^19.
|
|
49
|
-
"react-dom": "^19.
|
|
50
|
-
"vite": "^8.0.
|
|
48
|
+
"react": "^19.2.7",
|
|
49
|
+
"react-dom": "^19.2.7",
|
|
50
|
+
"vite": "^8.0.16"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/server/aggregator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CACHE_ASSUMPTIONS } from './cost-catalog.js';
|
|
2
|
-
import { createDayKeyFormatter } from './day-key.js';
|
|
2
|
+
import { createDayKeyFormatter, splitIntervalByDay } from './day-key.js';
|
|
3
3
|
|
|
4
4
|
function normalizeEffortKey(effort) {
|
|
5
5
|
if (!effort) return 'unknown';
|
|
@@ -231,7 +231,6 @@ function buildFamilies(sessions) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
function buildDaily(rawSessions, groupedSessions, tz) {
|
|
234
|
-
const toDayKeyInTz = createDayKeyFormatter(tz);
|
|
235
234
|
const dayMap = new Map();
|
|
236
235
|
for (const s of rawSessions) {
|
|
237
236
|
if (!s.started_at || !s.ended_at) continue;
|
|
@@ -241,23 +240,12 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
241
240
|
const totalDur = endMs - startMs;
|
|
242
241
|
if (totalDur <= 0) continue;
|
|
243
242
|
|
|
244
|
-
const startDay = toDayKeyInTz(startMs);
|
|
245
|
-
const endDay = toDayKeyInTz(endMs - 1);
|
|
246
|
-
|
|
247
243
|
if (s.has_usage_by_day) {
|
|
248
|
-
addSessionPresence(dayMap,
|
|
244
|
+
addSessionPresence(dayMap, startMs, endMs, totalDur, tz, s);
|
|
249
245
|
addUsageByDay(dayMap, s);
|
|
250
|
-
} else if (startDay === endDay) {
|
|
251
|
-
addToDay(dayMap, startDay, s, 1.0);
|
|
252
246
|
} else {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const nextDay = cursor + 86400000;
|
|
256
|
-
const overlapStart = Math.max(cursor, startMs);
|
|
257
|
-
const overlapEnd = Math.min(nextDay, endMs);
|
|
258
|
-
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
259
|
-
if (fraction > 0) addToDay(dayMap, toDayKeyInTz(cursor), s, fraction);
|
|
260
|
-
cursor = nextDay;
|
|
247
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
248
|
+
addToDay(dayMap, dayKey, s, overlapMs / totalDur);
|
|
261
249
|
}
|
|
262
250
|
}
|
|
263
251
|
}
|
|
@@ -320,6 +308,9 @@ function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
|
320
308
|
const dayMap = new Map();
|
|
321
309
|
for (const s of rawSessions) {
|
|
322
310
|
if (!s.started_at) continue;
|
|
311
|
+
const startMs = s.started_at * 1000;
|
|
312
|
+
const endMs = (s.ended_at || s.started_at) * 1000;
|
|
313
|
+
const totalDur = endMs - startMs;
|
|
323
314
|
if (s.has_usage_by_day) {
|
|
324
315
|
for (const usageDay of s.usage_by_day || []) {
|
|
325
316
|
const dayKey = usageDay.day;
|
|
@@ -328,16 +319,13 @@ function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
|
328
319
|
d.tokens += usageDay.tokens || 0;
|
|
329
320
|
if (usageDay.cost !== null) d.cost += usageDay.cost;
|
|
330
321
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
322
|
+
addHeatmapPresence(dayMap, s, startMs, endMs, totalDur, tz, toDayKeyInTz);
|
|
323
|
+
} else if (totalDur > 0) {
|
|
324
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
325
|
+
addHeatmapUsage(dayMap, dayKey, s, overlapMs / totalDur);
|
|
326
|
+
}
|
|
334
327
|
} else {
|
|
335
|
-
|
|
336
|
-
if (!dayMap.has(dk)) dayMap.set(dk, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
337
|
-
const d = dayMap.get(dk);
|
|
338
|
-
d.tokens += s.tokens_used;
|
|
339
|
-
if (s.cost !== null) d.cost += s.cost;
|
|
340
|
-
d.sessions++;
|
|
328
|
+
addHeatmapUsage(dayMap, toDayKeyInTz(startMs), s, 1);
|
|
341
329
|
}
|
|
342
330
|
}
|
|
343
331
|
|
|
@@ -351,20 +339,30 @@ function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
|
351
339
|
return Object.fromEntries(dayMap);
|
|
352
340
|
}
|
|
353
341
|
|
|
354
|
-
function
|
|
355
|
-
if (
|
|
356
|
-
|
|
342
|
+
function addHeatmapUsage(dayMap, dayKey, session, fraction) {
|
|
343
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
344
|
+
const d = dayMap.get(dayKey);
|
|
345
|
+
d.tokens += session.tokens_used * fraction;
|
|
346
|
+
if (session.cost !== null) d.cost += session.cost * fraction;
|
|
347
|
+
if (fraction > 0.001) d.sessions++;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function addHeatmapPresence(dayMap, session, startMs, endMs, totalDur, tz, toDayKeyInTz) {
|
|
351
|
+
if (totalDur <= 0) {
|
|
352
|
+
const dayKey = toDayKeyInTz(startMs);
|
|
353
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
354
|
+
dayMap.get(dayKey).sessions++;
|
|
357
355
|
return;
|
|
358
356
|
}
|
|
357
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
358
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
359
|
+
if ((overlapMs / totalDur) > 0.001) dayMap.get(dayKey).sessions++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
359
362
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const overlapStart = Math.max(cursor, startMs);
|
|
364
|
-
const overlapEnd = Math.min(nextDay, endMs);
|
|
365
|
-
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
366
|
-
if (fraction > 0) addPresenceToDay(dayMap, toDayKeyInTz(cursor), session, fraction);
|
|
367
|
-
cursor = nextDay;
|
|
363
|
+
function addSessionPresence(dayMap, startMs, endMs, totalDur, tz, session) {
|
|
364
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
365
|
+
addPresenceToDay(dayMap, dayKey, session, overlapMs / totalDur);
|
|
368
366
|
}
|
|
369
367
|
}
|
|
370
368
|
|
|
@@ -426,11 +424,6 @@ function addElapsedToDay(dayMap, dayKey, session, seconds) {
|
|
|
426
424
|
d.by_repo[rKey].elapsed_seconds += seconds;
|
|
427
425
|
}
|
|
428
426
|
|
|
429
|
-
function dayStartMs(dayKey) {
|
|
430
|
-
const [y, m, d] = dayKey.split('-').map(Number);
|
|
431
|
-
return new Date(y, m - 1, d).getTime();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
427
|
function overlapsLowerBound(session, lowerBound) {
|
|
435
428
|
const startedAt = session.started_at || 0;
|
|
436
429
|
const endedAt = session.ended_at || startedAt;
|
package/server/day-key.js
CHANGED
|
@@ -14,3 +14,47 @@ export function createDayKeyFormatter(tz) {
|
|
|
14
14
|
return `${year}-${month}-${day}`;
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export function addDaysToDayKey(dayKey, days) {
|
|
19
|
+
const [year, month, day] = dayKey.split('-').map(Number);
|
|
20
|
+
const date = new Date(Date.UTC(year, month - 1, day + days));
|
|
21
|
+
return date.toISOString().slice(0, 10);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getZonedDayStartMs(dayKey, tz) {
|
|
25
|
+
const toDayKey = createDayKeyFormatter(tz);
|
|
26
|
+
const guess = Date.parse(`${dayKey}T00:00:00Z`);
|
|
27
|
+
let low = guess - 36 * 60 * 60 * 1000;
|
|
28
|
+
let high = guess + 36 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
while (low < high) {
|
|
31
|
+
const mid = Math.floor((low + high) / 2);
|
|
32
|
+
if (toDayKey(mid) < dayKey) low = mid + 1;
|
|
33
|
+
else high = mid;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return low;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function splitIntervalByDay(startMs, endMs, tz) {
|
|
40
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return [];
|
|
41
|
+
|
|
42
|
+
const toDayKey = createDayKeyFormatter(tz);
|
|
43
|
+
const result = [];
|
|
44
|
+
let cursor = startMs;
|
|
45
|
+
let dayKey = toDayKey(cursor);
|
|
46
|
+
|
|
47
|
+
while (cursor < endMs) {
|
|
48
|
+
const nextDayStart = getZonedDayStartMs(addDaysToDayKey(dayKey, 1), tz);
|
|
49
|
+
const overlapEnd = Math.min(endMs, nextDayStart);
|
|
50
|
+
if (overlapEnd > cursor) {
|
|
51
|
+
result.push({ dayKey, overlapMs: overlapEnd - cursor });
|
|
52
|
+
cursor = overlapEnd;
|
|
53
|
+
} else {
|
|
54
|
+
cursor = Math.min(endMs, cursor + 1);
|
|
55
|
+
}
|
|
56
|
+
dayKey = toDayKey(cursor);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
package/server/live-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CACHE_ASSUMPTIONS } from './cost-catalog.js';
|
|
2
|
-
import { createDayKeyFormatter } from './day-key.js';
|
|
2
|
+
import { createDayKeyFormatter, splitIntervalByDay } from './day-key.js';
|
|
3
3
|
|
|
4
4
|
export function createLiveAggregateState(tz) {
|
|
5
5
|
const now = Date.now() / 1000;
|
|
@@ -64,8 +64,8 @@ export function applySessionToLiveState(live, session, patch) {
|
|
|
64
64
|
applyFamilyBucket(live.families[rangeKey], live.familyTopKeys[rangeKey], session, patch.families[rangeKey]);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
applyDailyBucket(live.daily, session, live.toDayKey, patch.daily);
|
|
68
|
-
applyHeatmapBucket(live.heatmap, session, live.toDayKey, patch.heatmap);
|
|
67
|
+
applyDailyBucket(live.daily, session, live.tz, live.toDayKey, patch.daily);
|
|
68
|
+
applyHeatmapBucket(live.heatmap, session, live.tz, live.toDayKey, patch.heatmap);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function buildLiveBootstrap(live) {
|
|
@@ -218,35 +218,20 @@ function applyFamilyBucket(familyMap, topKeys, session, dirtySet) {
|
|
|
218
218
|
dirtySet.add(key);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function applyDailyBucket(dayMap, session, toDayKey, dirtySet) {
|
|
221
|
+
function applyDailyBucket(dayMap, session, tz, toDayKey, dirtySet) {
|
|
222
222
|
if (!session.started_at || !session.ended_at) return;
|
|
223
223
|
|
|
224
224
|
const startMs = session.started_at * 1000;
|
|
225
225
|
const endMs = session.ended_at * 1000;
|
|
226
226
|
const totalDur = endMs - startMs;
|
|
227
227
|
if (totalDur > 0) {
|
|
228
|
-
const startDay = toDayKey(startMs);
|
|
229
|
-
const endDay = toDayKey(endMs - 1);
|
|
230
|
-
|
|
231
228
|
if (session.has_usage_by_day) {
|
|
232
|
-
addSessionPresence(dayMap,
|
|
229
|
+
addSessionPresence(dayMap, startMs, endMs, totalDur, tz, session, dirtySet);
|
|
233
230
|
addUsageByDay(dayMap, session, dirtySet);
|
|
234
|
-
} else if (startDay === endDay) {
|
|
235
|
-
addToDay(dayMap, startDay, session, 1.0);
|
|
236
|
-
dirtySet.add(startDay);
|
|
237
231
|
} else {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const overlapStart = Math.max(cursor, startMs);
|
|
242
|
-
const overlapEnd = Math.min(nextDay, endMs);
|
|
243
|
-
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
244
|
-
if (fraction > 0) {
|
|
245
|
-
const dayKey = toDayKey(cursor);
|
|
246
|
-
addToDay(dayMap, dayKey, session, fraction);
|
|
247
|
-
dirtySet.add(dayKey);
|
|
248
|
-
}
|
|
249
|
-
cursor = nextDay;
|
|
232
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
233
|
+
addToDay(dayMap, dayKey, session, overlapMs / totalDur);
|
|
234
|
+
dirtySet.add(dayKey);
|
|
250
235
|
}
|
|
251
236
|
}
|
|
252
237
|
}
|
|
@@ -259,7 +244,11 @@ function applyDailyBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
259
244
|
}
|
|
260
245
|
}
|
|
261
246
|
|
|
262
|
-
function applyHeatmapBucket(dayMap, session, toDayKey, dirtySet) {
|
|
247
|
+
function applyHeatmapBucket(dayMap, session, tz, toDayKey, dirtySet) {
|
|
248
|
+
const startMs = session.started_at ? session.started_at * 1000 : null;
|
|
249
|
+
const endMs = (session.ended_at || session.started_at) ? (session.ended_at || session.started_at) * 1000 : null;
|
|
250
|
+
const totalDur = endMs - startMs;
|
|
251
|
+
|
|
263
252
|
if (session.has_usage_by_day) {
|
|
264
253
|
for (const usageDay of session.usage_by_day || []) {
|
|
265
254
|
const dayKey = usageDay.day;
|
|
@@ -268,19 +257,38 @@ function applyHeatmapBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
268
257
|
if (usageDay.cost !== null) day.cost += usageDay.cost;
|
|
269
258
|
dirtySet.add(dayKey);
|
|
270
259
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
260
|
+
if (startMs !== null) {
|
|
261
|
+
if (totalDur > 0) {
|
|
262
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
263
|
+
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
264
|
+
if ((overlapMs / totalDur) > 0.001) day.sessions += 1;
|
|
265
|
+
dirtySet.add(dayKey);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const startDay = toDayKey(startMs);
|
|
269
|
+
const day = ensureHeatmapDay(dayMap, startDay);
|
|
270
|
+
day.sessions += 1;
|
|
271
|
+
dirtySet.add(startDay);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else if (startMs !== null) {
|
|
275
|
+
if (totalDur > 0) {
|
|
276
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
277
|
+
const fraction = overlapMs / totalDur;
|
|
278
|
+
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
279
|
+
day.tokens += (session.tokens_used || 0) * fraction;
|
|
280
|
+
if (session.cost !== null) day.cost += session.cost * fraction;
|
|
281
|
+
if (fraction > 0.001) day.sessions += 1;
|
|
282
|
+
dirtySet.add(dayKey);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const dayKey = toDayKey(startMs);
|
|
286
|
+
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
287
|
+
day.tokens += session.tokens_used || 0;
|
|
288
|
+
if (session.cost !== null) day.cost += session.cost;
|
|
274
289
|
day.sessions += 1;
|
|
275
|
-
dirtySet.add(
|
|
290
|
+
dirtySet.add(dayKey);
|
|
276
291
|
}
|
|
277
|
-
} else if (session.started_at) {
|
|
278
|
-
const dayKey = toDayKey(session.started_at * 1000);
|
|
279
|
-
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
280
|
-
day.tokens += session.tokens_used || 0;
|
|
281
|
-
if (session.cost !== null) day.cost += session.cost;
|
|
282
|
-
day.sessions += 1;
|
|
283
|
-
dirtySet.add(dayKey);
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
if (session.active_by_day) {
|
|
@@ -292,25 +300,10 @@ function applyHeatmapBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
292
300
|
}
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
function addSessionPresence(dayMap,
|
|
296
|
-
|
|
297
|
-
addPresenceToDay(dayMap,
|
|
298
|
-
dirtySet.add(
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
let cursor = dayStartMs(startDay);
|
|
303
|
-
while (cursor < endMs) {
|
|
304
|
-
const nextDay = cursor + 86400000;
|
|
305
|
-
const overlapStart = Math.max(cursor, startMs);
|
|
306
|
-
const overlapEnd = Math.min(nextDay, endMs);
|
|
307
|
-
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
308
|
-
if (fraction > 0) {
|
|
309
|
-
const dayKey = toDayKey(cursor);
|
|
310
|
-
addPresenceToDay(dayMap, dayKey, session, fraction);
|
|
311
|
-
dirtySet.add(dayKey);
|
|
312
|
-
}
|
|
313
|
-
cursor = nextDay;
|
|
303
|
+
function addSessionPresence(dayMap, startMs, endMs, totalDur, tz, session, dirtySet) {
|
|
304
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
305
|
+
addPresenceToDay(dayMap, dayKey, session, overlapMs / totalDur);
|
|
306
|
+
dirtySet.add(dayKey);
|
|
314
307
|
}
|
|
315
308
|
}
|
|
316
309
|
|
|
@@ -587,8 +580,3 @@ function overlapsLowerBound(session, lowerBound) {
|
|
|
587
580
|
const endedAt = session.ended_at || startedAt;
|
|
588
581
|
return endedAt >= lowerBound;
|
|
589
582
|
}
|
|
590
|
-
|
|
591
|
-
function dayStartMs(dayKey) {
|
|
592
|
-
const [y, m, d] = dayKey.split('-').map(Number);
|
|
593
|
-
return new Date(y, m - 1, d).getTime();
|
|
594
|
-
}
|
package/server/rollout-reader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'fs';
|
|
2
2
|
import { readFile } from 'fs/promises';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import { createDayKeyFormatter } from './day-key.js';
|
|
4
|
+
import { createDayKeyFormatter, splitIntervalByDay } from './day-key.js';
|
|
5
5
|
|
|
6
6
|
const ACTIVE_GAP_CAP_MS = 15 * 60 * 1000;
|
|
7
7
|
const LINE_HEADER_SCAN_CHARS = 512;
|
|
@@ -13,6 +13,7 @@ const RG_RELEVANT_PATTERN =
|
|
|
13
13
|
'"type"\\s*:\\s*"token_count"';
|
|
14
14
|
const RG_TOKEN_COUNT_PATTERN = '"type"\\s*:\\s*"token_count"';
|
|
15
15
|
const RG_TIMESTAMP_PATTERN = '^\\{[^{}]*"timestamp"\\s*:\\s*"[^"]+"';
|
|
16
|
+
const RG_ANY_TIMESTAMP_PATTERN = '"timestamp"\\s*:\\s*"[^"]+"';
|
|
16
17
|
|
|
17
18
|
export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
18
19
|
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
@@ -21,7 +22,7 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
21
22
|
|
|
22
23
|
const tz = opts.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
23
24
|
const toDayKey = opts.toDayKey || createDayKeyFormatter(tz);
|
|
24
|
-
|
|
25
|
+
let fastScan = opts.fastScan === true;
|
|
25
26
|
const rgScan = opts.rgScan === true;
|
|
26
27
|
const rgMinBytes = Math.max(0, Number(opts.rgMinBytes ?? DEFAULT_RG_MIN_BYTES) || 0);
|
|
27
28
|
const result = {
|
|
@@ -44,10 +45,12 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
44
45
|
let activeFromRgTimestamps = false;
|
|
45
46
|
if (rgScan && shouldUseRipgrep(rolloutPath, rgMinBytes)) {
|
|
46
47
|
const rgResult = await readRolloutLinesWithRipgrep(rolloutPath);
|
|
47
|
-
if (rgResult) {
|
|
48
|
+
if (rgResult?.timestampMatches?.length && rgResult.timestampsComplete) {
|
|
48
49
|
lines = rgResult.relevantLines;
|
|
49
|
-
applyTimestampMatches(result, rgResult.timestampMatches,
|
|
50
|
+
applyTimestampMatches(result, rgResult.timestampMatches, tz);
|
|
50
51
|
activeFromRgTimestamps = true;
|
|
52
|
+
} else if (rgResult) {
|
|
53
|
+
fastScan = false;
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
if (!lines) {
|
|
@@ -74,33 +77,32 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
74
77
|
|
|
75
78
|
if (fastScan || activeFromRgTimestamps) {
|
|
76
79
|
ts = extractTimestampMs(line);
|
|
77
|
-
|
|
80
|
+
const countedHeaderTimestamp = ts !== null && !activeFromRgTimestamps;
|
|
81
|
+
if (countedHeaderTimestamp) {
|
|
78
82
|
updateTimestampBounds(result, ts);
|
|
79
83
|
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
80
|
-
|
|
81
|
-
activeMs += deltaMs;
|
|
82
|
-
if (deltaMs > 0) {
|
|
83
|
-
const dayKey = toDayKey(prevTimestamp);
|
|
84
|
-
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + deltaMs);
|
|
85
|
-
}
|
|
84
|
+
activeMs += addActiveInterval(activeByDay, prevTimestamp, ts, tz);
|
|
86
85
|
}
|
|
87
86
|
prevTimestamp = ts;
|
|
88
87
|
}
|
|
89
88
|
if (!activeFromRgTimestamps && !isRolloutLineWorthParsing(line)) continue;
|
|
90
89
|
obj = JSON.parse(line);
|
|
90
|
+
if (ts === null && obj.timestamp) ts = parseTimestampMs(obj.timestamp);
|
|
91
|
+
if (ts !== null && !activeFromRgTimestamps && !countedHeaderTimestamp) {
|
|
92
|
+
updateTimestampBounds(result, ts);
|
|
93
|
+
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
94
|
+
activeMs += addActiveInterval(activeByDay, prevTimestamp, ts, tz);
|
|
95
|
+
}
|
|
96
|
+
prevTimestamp = ts;
|
|
97
|
+
}
|
|
91
98
|
} else {
|
|
92
99
|
obj = JSON.parse(line);
|
|
93
100
|
if (obj.timestamp) {
|
|
94
|
-
ts =
|
|
95
|
-
if (
|
|
101
|
+
ts = parseTimestampMs(obj.timestamp);
|
|
102
|
+
if (ts !== null) {
|
|
96
103
|
updateTimestampBounds(result, ts);
|
|
97
104
|
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
98
|
-
|
|
99
|
-
activeMs += deltaMs;
|
|
100
|
-
if (deltaMs > 0) {
|
|
101
|
-
const dayKey = toDayKey(prevTimestamp);
|
|
102
|
-
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + deltaMs);
|
|
103
|
-
}
|
|
105
|
+
activeMs += addActiveInterval(activeByDay, prevTimestamp, ts, tz);
|
|
104
106
|
}
|
|
105
107
|
prevTimestamp = ts;
|
|
106
108
|
}
|
|
@@ -299,12 +301,17 @@ function shouldUseRipgrep(rolloutPath, minBytes) {
|
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
async function readRolloutLinesWithRipgrep(rolloutPath) {
|
|
302
|
-
const [relevantLines, timestampMatches] = await Promise.all([
|
|
304
|
+
const [relevantLines, timestampMatches, timestampMentions] = await Promise.all([
|
|
303
305
|
readMatchingLinesWithRipgrep(RG_RELEVANT_PATTERN, rolloutPath),
|
|
304
306
|
readMatchingLinesWithRipgrep(RG_TIMESTAMP_PATTERN, rolloutPath, ['--only-matching']),
|
|
307
|
+
readMatchingLinesWithRipgrep(RG_ANY_TIMESTAMP_PATTERN, rolloutPath, ['--only-matching']),
|
|
305
308
|
]);
|
|
306
|
-
if (!relevantLines || !timestampMatches) return null;
|
|
307
|
-
return {
|
|
309
|
+
if (!relevantLines || !timestampMatches || !timestampMentions) return null;
|
|
310
|
+
return {
|
|
311
|
+
relevantLines,
|
|
312
|
+
timestampMatches,
|
|
313
|
+
timestampsComplete: timestampMatches.length === timestampMentions.length,
|
|
314
|
+
};
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
function readMatchingLinesWithRipgrep(pattern, rolloutPath, extraArgs = []) {
|
|
@@ -336,7 +343,7 @@ function readMatchingLinesWithRipgrep(pattern, rolloutPath, extraArgs = []) {
|
|
|
336
343
|
});
|
|
337
344
|
}
|
|
338
345
|
|
|
339
|
-
function applyTimestampMatches(result, timestampMatches,
|
|
346
|
+
function applyTimestampMatches(result, timestampMatches, tz) {
|
|
340
347
|
let prevTimestamp = null;
|
|
341
348
|
let activeMs = 0;
|
|
342
349
|
const activeByDay = new Map();
|
|
@@ -346,12 +353,7 @@ function applyTimestampMatches(result, timestampMatches, toDayKey) {
|
|
|
346
353
|
if (ts === null) continue;
|
|
347
354
|
updateTimestampBounds(result, ts);
|
|
348
355
|
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
349
|
-
|
|
350
|
-
activeMs += deltaMs;
|
|
351
|
-
if (deltaMs > 0) {
|
|
352
|
-
const dayKey = toDayKey(prevTimestamp);
|
|
353
|
-
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + deltaMs);
|
|
354
|
-
}
|
|
356
|
+
activeMs += addActiveInterval(activeByDay, prevTimestamp, ts, tz);
|
|
355
357
|
}
|
|
356
358
|
prevTimestamp = ts;
|
|
357
359
|
}
|
|
@@ -365,6 +367,15 @@ function applyTimestampMatches(result, timestampMatches, toDayKey) {
|
|
|
365
367
|
}
|
|
366
368
|
}
|
|
367
369
|
|
|
370
|
+
function addActiveInterval(activeByDay, startMs, endMs, tz) {
|
|
371
|
+
const cappedEndMs = Math.min(endMs, startMs + ACTIVE_GAP_CAP_MS);
|
|
372
|
+
if (cappedEndMs <= startMs) return 0;
|
|
373
|
+
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, cappedEndMs, tz)) {
|
|
374
|
+
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + overlapMs);
|
|
375
|
+
}
|
|
376
|
+
return cappedEndMs - startMs;
|
|
377
|
+
}
|
|
378
|
+
|
|
368
379
|
function updateTimestampBounds(result, ts) {
|
|
369
380
|
if (!result.first_timestamp || ts < result.first_timestamp) {
|
|
370
381
|
result.first_timestamp = ts;
|
|
@@ -380,7 +391,11 @@ function extractTimestampMs(line) {
|
|
|
380
391
|
: line;
|
|
381
392
|
const match = TIMESTAMP_RE.exec(head);
|
|
382
393
|
if (!match) return null;
|
|
383
|
-
|
|
394
|
+
return parseTimestampMs(match[1]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseTimestampMs(value) {
|
|
398
|
+
const ts = Date.parse(value);
|
|
384
399
|
return Number.isNaN(ts) ? null : ts;
|
|
385
400
|
}
|
|
386
401
|
|
package/server/sqlite-reader.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
const require = createRequire(import.meta.url);
|
|
6
|
-
|
|
7
5
|
export function readThreads(codexHome, onProgress) {
|
|
8
6
|
const dbPath = path.join(codexHome, 'state_5.sqlite');
|
|
9
7
|
if (!existsSync(dbPath)) {
|
|
10
8
|
throw new Error(`SQLite database not found at ${dbPath}`);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
const db = new Database(dbPath, { readonly: true });
|
|
11
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
15
12
|
|
|
16
13
|
try {
|
|
17
14
|
const count = db.prepare('SELECT count(*) as c FROM threads').get().c;
|