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/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-BfQCWE9K.js"></script>
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.21",
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": "^20.19.0 || >=22.12.0"
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
- "better-sqlite3": "^12.8.0",
32
- "commander": "^14.0.3",
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.2.0",
35
+ "ffmpeg-static": "^5.3.0",
36
36
  "open": "^11.0.0",
37
- "playwright-core": "^1.58.2"
37
+ "playwright-core": "^1.61.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@tanstack/react-table": "^8.21.3",
41
- "@tanstack/react-virtual": "^3.13.23",
42
- "@types/react": "^19.0.0",
43
- "@types/react-dom": "^19.0.0",
44
- "@vitejs/plugin-react": "^6.0.1",
45
- "echarts": "^6.0.0",
46
- "echarts-for-react": "^3.0.2",
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.1.0",
49
- "react-dom": "^19.1.0",
50
- "vite": "^8.0.0"
48
+ "react": "^19.2.7",
49
+ "react-dom": "^19.2.7",
50
+ "vite": "^8.0.16"
51
51
  }
52
52
  }
@@ -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, startDay, endDay, startMs, endMs, totalDur, toDayKeyInTz, s);
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
- let cursor = dayStartMs(startDay);
254
- while (cursor < endMs) {
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
- const startDay = toDayKeyInTz(s.started_at * 1000);
332
- if (!dayMap.has(startDay)) dayMap.set(startDay, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
333
- dayMap.get(startDay).sessions++;
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
- const dk = toDayKeyInTz(s.started_at * 1000);
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 addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur, toDayKeyInTz, session) {
355
- if (startDay === endDay) {
356
- addPresenceToDay(dayMap, startDay, session, 1.0);
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
- let cursor = dayStartMs(startDay);
361
- while (cursor < endMs) {
362
- const nextDay = cursor + 86400000;
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
+ }
@@ -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, startDay, endDay, startMs, endMs, totalDur, toDayKey, session, dirtySet);
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
- let cursor = dayStartMs(startDay);
239
- while (cursor < endMs) {
240
- const nextDay = cursor + 86400000;
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 (session.started_at) {
272
- const startDay = toDayKey(session.started_at * 1000);
273
- const day = ensureHeatmapDay(dayMap, startDay);
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(startDay);
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, startDay, endDay, startMs, endMs, totalDur, toDayKey, session, dirtySet) {
296
- if (startDay === endDay) {
297
- addPresenceToDay(dayMap, startDay, session, 1.0);
298
- dirtySet.add(startDay);
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
- }
@@ -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
- const fastScan = opts.fastScan === true;
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, toDayKey);
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
- if (ts !== null && !activeFromRgTimestamps) {
80
+ const countedHeaderTimestamp = ts !== null && !activeFromRgTimestamps;
81
+ if (countedHeaderTimestamp) {
78
82
  updateTimestampBounds(result, ts);
79
83
  if (prevTimestamp !== null && ts >= prevTimestamp) {
80
- const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
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 = new Date(obj.timestamp).getTime();
95
- if (!isNaN(ts)) {
101
+ ts = parseTimestampMs(obj.timestamp);
102
+ if (ts !== null) {
96
103
  updateTimestampBounds(result, ts);
97
104
  if (prevTimestamp !== null && ts >= prevTimestamp) {
98
- const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
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 { relevantLines, timestampMatches };
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, toDayKey) {
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
- const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
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
- const ts = Date.parse(match[1]);
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
 
@@ -1,17 +1,14 @@
1
- import { createRequire } from 'module';
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 Database = require('better-sqlite3');
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;