ccc-notifier 0.3.0 → 0.5.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.
@@ -4,7 +4,7 @@ import {
4
4
  isMuted,
5
5
  readMuteState,
6
6
  writeMuteState
7
- } from "./chunk-ECADO26T.js";
7
+ } from "./chunk-5PH7PPD6.js";
8
8
 
9
9
  // src/mute.ts
10
10
  function parseDuration(arg) {
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ paths
4
+ } from "./chunk-5PH7PPD6.js";
5
+
6
+ // src/dashboard-state.ts
7
+ import {
8
+ closeSync,
9
+ existsSync,
10
+ openSync,
11
+ readFileSync,
12
+ readSync,
13
+ renameSync,
14
+ rmSync,
15
+ writeFileSync
16
+ } from "fs";
17
+ import { randomUUID } from "crypto";
18
+ function localDate(now) {
19
+ const y = now.getFullYear();
20
+ const m = String(now.getMonth() + 1).padStart(2, "0");
21
+ const d = String(now.getDate()).padStart(2, "0");
22
+ return `${y}-${m}-${d}`;
23
+ }
24
+ function timeZone() {
25
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
26
+ }
27
+ function makeFullDashboardState(now = /* @__PURE__ */ new Date()) {
28
+ return { localDate: localDate(now), timeZone: timeZone(), generatedAt: now.toISOString() };
29
+ }
30
+ function readState() {
31
+ const file = paths().dashboardFullStateFile;
32
+ if (!existsSync(file)) return null;
33
+ try {
34
+ const value = JSON.parse(readFileSync(file, "utf8"));
35
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
36
+ const v = value;
37
+ if (typeof v.localDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v.localDate) || typeof v.timeZone !== "string" || v.timeZone.length === 0 || typeof v.generatedAt !== "string" || !Number.isFinite(Date.parse(v.generatedAt))) {
38
+ return null;
39
+ }
40
+ return v;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function isFullDashboardDue(now = /* @__PURE__ */ new Date()) {
46
+ const p = paths();
47
+ if (!existsSync(p.fullDashboardFile)) return true;
48
+ try {
49
+ const fd = openSync(p.fullDashboardFile, "r");
50
+ try {
51
+ const head = Buffer.alloc(512);
52
+ const n = readSync(fd, head, 0, head.length, 0);
53
+ if (head.toString("utf8", 0, n).includes('name="cccn-placeholder"')) return true;
54
+ } finally {
55
+ closeSync(fd);
56
+ }
57
+ } catch {
58
+ return true;
59
+ }
60
+ const state = readState();
61
+ if (state === null) return true;
62
+ const expected = makeFullDashboardState(now);
63
+ if (state.timeZone !== expected.timeZone) return true;
64
+ if (state.localDate !== expected.localDate) return true;
65
+ if (state.localDate > expected.localDate) return true;
66
+ if (Date.parse(state.generatedAt) > now.getTime()) return true;
67
+ return false;
68
+ }
69
+ function writeFullDashboardStateAtomic(state) {
70
+ const file = paths().dashboardFullStateFile;
71
+ const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
72
+ try {
73
+ writeFileSync(tmp, `${JSON.stringify(state)}
74
+ `, "utf8");
75
+ renameSync(tmp, file);
76
+ } finally {
77
+ rmSync(tmp, { force: true });
78
+ }
79
+ }
80
+ function invalidateCanonicalDashboards() {
81
+ const p = paths();
82
+ for (const file of [p.recentDashboardFile, p.fullDashboardFile, p.dashboardFullStateFile]) {
83
+ rmSync(file, { force: true });
84
+ }
85
+ }
86
+
87
+ export {
88
+ makeFullDashboardState,
89
+ isFullDashboardDue,
90
+ writeFullDashboardStateAtomic,
91
+ invalidateCanonicalDashboards
92
+ };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/codex/sessions.ts
4
+ import { promises as fsp } from "fs";
5
+ import { join, resolve } from "path";
6
+ var CODEX_MAX_DEPTH = 4;
7
+ async function listCodexRollouts(sessionsRoot) {
8
+ const rollouts = [];
9
+ let unreadableDirs = 0;
10
+ const root = resolve(sessionsRoot);
11
+ const rootStat = await fsp.lstat(root).catch(() => null);
12
+ if (rootStat === null || !rootStat.isDirectory()) {
13
+ return { rollouts, unreadableDirs: 1 };
14
+ }
15
+ const walk = async (dir, depth) => {
16
+ const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
17
+ if (entries === null) {
18
+ unreadableDirs += 1;
19
+ return;
20
+ }
21
+ for (const entry of entries) {
22
+ const full = resolve(join(dir, entry.name));
23
+ if (entry.isFile()) {
24
+ if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) rollouts.push(full);
25
+ } else if (entry.isDirectory() && depth < CODEX_MAX_DEPTH) {
26
+ await walk(full, depth + 1);
27
+ }
28
+ }
29
+ };
30
+ await walk(root, 1);
31
+ return { rollouts, unreadableDirs };
32
+ }
33
+ async function findLatestCodexRollout(sessionsRoot) {
34
+ const discovery = await listCodexRollouts(sessionsRoot);
35
+ let latest = null;
36
+ let latestMtime = -Infinity;
37
+ let unreadableFiles = 0;
38
+ for (const path of discovery.rollouts) {
39
+ const stat = await fsp.lstat(path).catch(() => null);
40
+ if (stat === null || !stat.isFile()) {
41
+ unreadableFiles += 1;
42
+ continue;
43
+ }
44
+ if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && (latest === null || path < latest)) {
45
+ latest = path;
46
+ latestMtime = stat.mtimeMs;
47
+ }
48
+ }
49
+ return { latest, unreadableDirs: discovery.unreadableDirs, unreadableFiles };
50
+ }
51
+
52
+ export {
53
+ listCodexRollouts,
54
+ findLatestCodexRollout
55
+ };
@@ -61,7 +61,7 @@ function resolvePrice(modelId, table) {
61
61
  return bestPrice ? { ...bestPrice } : null;
62
62
  }
63
63
  function computeCost(main, sidechain, table) {
64
- const byModel = {};
64
+ const byModel = /* @__PURE__ */ Object.create(null);
65
65
  const unknownModels = [];
66
66
  let usd = 0;
67
67
  const accumulate = (usage) => {
@@ -73,7 +73,7 @@ function computeCost(main, sidechain, table) {
73
73
  } else {
74
74
  cost = (tokens.input * p.input + tokens.output * p.output + tokens.cacheWrite5m * p.cacheWrite5m + tokens.cacheWrite1h * p.cacheWrite1h + tokens.cacheRead * p.cacheRead) / 1e6;
75
75
  }
76
- byModel[model] = (byModel[model] ?? 0) + cost;
76
+ byModel[model] = (Object.hasOwn(byModel, model) ? byModel[model] : 0) + cost;
77
77
  usd += cost;
78
78
  }
79
79
  };
@@ -277,11 +277,10 @@ async function getUsdJpy(cfg, cacheDir) {
277
277
  return { rate: cfg.fx.fallbackRate, source: "fixed", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
278
278
  }
279
279
 
280
- // src/transcript.ts
280
+ // src/codex/transcript.ts
281
281
  import { readFile as readFile2 } from "fs/promises";
282
+ import { basename } from "path";
282
283
  var NEWLINE = 10;
283
- var MAX_SEEN_KEYS = 500;
284
- var SYNTHETIC_MODEL = "<synthetic>";
285
284
  function isRecord(v) {
286
285
  return typeof v === "object" && v !== null && !Array.isArray(v);
287
286
  }
@@ -291,6 +290,280 @@ function numOf(v) {
291
290
  function strOrNull(v) {
292
291
  return typeof v === "string" ? v : null;
293
292
  }
293
+ function zeroTotals() {
294
+ return { input: 0, cached: 0, output: 0 };
295
+ }
296
+ function isZeroTotals(t) {
297
+ return t.input === 0 && t.cached === 0 && t.output === 0;
298
+ }
299
+ function addTotals(target, d) {
300
+ target.input += d.input;
301
+ target.cached += d.cached;
302
+ target.output += d.output;
303
+ }
304
+ function readTotals(v) {
305
+ if (!isRecord(v)) return null;
306
+ return {
307
+ input: numOf(v.input_tokens),
308
+ cached: numOf(v.cached_input_tokens),
309
+ output: numOf(v.output_tokens)
310
+ };
311
+ }
312
+ function totalsToBuckets(acc) {
313
+ return {
314
+ input: Math.max(0, acc.input - acc.cached),
315
+ output: acc.output,
316
+ cacheWrite5m: 0,
317
+ cacheWrite1h: 0,
318
+ cacheRead: acc.cached
319
+ };
320
+ }
321
+ function sessionIdFromFilename(rolloutPath) {
322
+ const m = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(
323
+ basename(rolloutPath)
324
+ );
325
+ return m !== null ? m[1] : "";
326
+ }
327
+ async function readAll(path2) {
328
+ try {
329
+ return await readFile2(path2);
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function newSegmentBuf() {
335
+ return {
336
+ acc: zeroTotals(),
337
+ apiCalls: 0,
338
+ prompt: null,
339
+ turnCtxCwd: null,
340
+ firstTs: null,
341
+ endTs: null,
342
+ hasLines: false
343
+ };
344
+ }
345
+ async function scanWindow(rolloutPath, cursor) {
346
+ const buffer = await readAll(rolloutPath);
347
+ if (buffer === null) return null;
348
+ const fileSize = buffer.length;
349
+ let startOffset;
350
+ let rescan;
351
+ if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
352
+ startOffset = cursor.offset;
353
+ rescan = false;
354
+ } else {
355
+ startOffset = 0;
356
+ rescan = cursor !== null;
357
+ }
358
+ const tsFloor = cursor?.lastTs ?? null;
359
+ const initTotals = cursor?.codexTotals;
360
+ let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
361
+ const acc = zeroTotals();
362
+ let apiCalls = 0;
363
+ let lastModel = null;
364
+ let windowPrompt = null;
365
+ let windowTurnCtxCwd = null;
366
+ let sessionMetaCwd = null;
367
+ let sessionMetaSid = null;
368
+ let firstTs = null;
369
+ let lastTs = null;
370
+ const segments = [];
371
+ let seg = newSegmentBuf();
372
+ const snapshotSegment = (endOffset) => ({
373
+ acc: seg.acc,
374
+ apiCalls: seg.apiCalls,
375
+ prompt: seg.prompt,
376
+ model: lastModel,
377
+ cwd: seg.turnCtxCwd ?? sessionMetaCwd,
378
+ firstTs: seg.firstTs,
379
+ endTs: seg.endTs,
380
+ endOffset,
381
+ prevAtEnd: { ...prev },
382
+ lastTsAtEnd: lastTs
383
+ });
384
+ const handleLine = (raw, endOffset) => {
385
+ if (raw.trim().length === 0) return;
386
+ let obj;
387
+ try {
388
+ obj = JSON.parse(raw);
389
+ } catch {
390
+ return;
391
+ }
392
+ if (!isRecord(obj)) return;
393
+ const ts = strOrNull(obj.timestamp);
394
+ if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
395
+ if (ts !== null) {
396
+ if (firstTs === null || ts < firstTs) firstTs = ts;
397
+ if (lastTs === null || ts > lastTs) lastTs = ts;
398
+ if (seg.firstTs === null || ts < seg.firstTs) seg.firstTs = ts;
399
+ seg.endTs = ts;
400
+ }
401
+ seg.hasLines = true;
402
+ const payload = isRecord(obj.payload) ? obj.payload : null;
403
+ if (payload === null) return;
404
+ const type = obj.type;
405
+ if (type === "session_meta") {
406
+ const sid = strOrNull(payload.session_id);
407
+ if (sid !== null) sessionMetaSid = sid;
408
+ const c = strOrNull(payload.cwd);
409
+ if (c !== null) sessionMetaCwd = c;
410
+ return;
411
+ }
412
+ if (type === "turn_context") {
413
+ const m = strOrNull(payload.model);
414
+ if (m !== null) lastModel = m;
415
+ const c = strOrNull(payload.cwd);
416
+ if (c !== null) {
417
+ seg.turnCtxCwd = c;
418
+ windowTurnCtxCwd = c;
419
+ }
420
+ return;
421
+ }
422
+ if (type !== "event_msg") return;
423
+ const kind = payload.type;
424
+ if (kind === "user_message") {
425
+ const msg = strOrNull(payload.message);
426
+ if (msg !== null) {
427
+ seg.prompt = msg;
428
+ windowPrompt = msg;
429
+ }
430
+ return;
431
+ }
432
+ if (kind === "token_count") {
433
+ const info = isRecord(payload.info) ? payload.info : null;
434
+ if (info === null) return;
435
+ const total = readTotals(info.total_token_usage);
436
+ if (total === null) return;
437
+ let step = {
438
+ input: total.input - prev.input,
439
+ cached: total.cached - prev.cached,
440
+ output: total.output - prev.output
441
+ };
442
+ if (step.input < 0 || step.cached < 0 || step.output < 0) {
443
+ step = readTotals(info.last_token_usage) ?? zeroTotals();
444
+ }
445
+ addTotals(acc, step);
446
+ addTotals(seg.acc, step);
447
+ prev = total;
448
+ if (!isZeroTotals(step)) {
449
+ apiCalls++;
450
+ seg.apiCalls++;
451
+ }
452
+ return;
453
+ }
454
+ if (kind === "task_complete") {
455
+ segments.push(snapshotSegment(endOffset));
456
+ seg = newSegmentBuf();
457
+ }
458
+ };
459
+ let lineStart = startOffset;
460
+ for (let pos = startOffset; pos < fileSize; pos++) {
461
+ if (buffer[pos] !== NEWLINE) continue;
462
+ handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
463
+ lineStart = pos + 1;
464
+ }
465
+ const newOffset = lineStart;
466
+ const open = seg.hasLines ? snapshotSegment(newOffset) : null;
467
+ return {
468
+ segments,
469
+ open,
470
+ acc,
471
+ prev,
472
+ apiCalls,
473
+ model: lastModel,
474
+ prompt: windowPrompt,
475
+ cwd: windowTurnCtxCwd ?? sessionMetaCwd,
476
+ sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
477
+ firstTs,
478
+ lastTs,
479
+ newOffset
480
+ };
481
+ }
482
+ function windowCursor(scan) {
483
+ return {
484
+ offset: scan.newOffset,
485
+ lastUuid: null,
486
+ // rollout に uuid 行は無い
487
+ lastTs: scan.lastTs,
488
+ seenMessageKeys: [],
489
+ // 去重は codexTotals の差分方式が担う
490
+ codexTotals: { ...scan.prev }
491
+ };
492
+ }
493
+ async function aggregateCodexTurn(rolloutPath, cursor) {
494
+ const scan = await scanWindow(rolloutPath, cursor);
495
+ if (scan === null || isZeroTotals(scan.acc)) return null;
496
+ return {
497
+ sessionId: scan.sessionId,
498
+ main: { [scan.model ?? "unknown"]: totalsToBuckets(scan.acc) },
499
+ sidechain: {},
500
+ // Codex にサブエージェント概念は無い
501
+ apiCalls: scan.apiCalls,
502
+ prompt: scan.prompt,
503
+ cwd: scan.cwd,
504
+ gitBranch: null,
505
+ // rollout に無い
506
+ firstTs: scan.firstTs,
507
+ lastTs: scan.lastTs,
508
+ newCursor: windowCursor(scan)
509
+ };
510
+ }
511
+ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
512
+ const scan = await scanWindow(rolloutPath, cursor);
513
+ if (scan === null || isZeroTotals(scan.acc)) return null;
514
+ const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
515
+ if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
516
+ const last = picked[picked.length - 1];
517
+ if (last !== void 0) {
518
+ addTotals(last.acc, scan.open.acc);
519
+ last.apiCalls += scan.open.apiCalls;
520
+ if (scan.open.endTs !== null) last.endTs = scan.open.endTs;
521
+ } else {
522
+ picked.push(scan.open);
523
+ }
524
+ }
525
+ const lastIndex = picked.length - 1;
526
+ return picked.map((s, i) => ({
527
+ agg: {
528
+ sessionId: scan.sessionId,
529
+ // session_meta はファイル先頭にしか無いので全ドラフト共通
530
+ main: { [s.model ?? "unknown"]: totalsToBuckets(s.acc) },
531
+ sidechain: {},
532
+ apiCalls: s.apiCalls,
533
+ prompt: s.prompt,
534
+ cwd: s.cwd,
535
+ gitBranch: null,
536
+ firstTs: s.firstTs,
537
+ lastTs: s.endTs,
538
+ // 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
539
+ // 末尾の usage ゼロな行の読み捨てもここに含まれる)。途中のドラフトはそのセグメント末尾を
540
+ // 指す有効な再開点(そこから読み直せば残りが差分になる)。
541
+ newCursor: i === lastIndex ? windowCursor(scan) : {
542
+ offset: s.endOffset,
543
+ lastUuid: null,
544
+ lastTs: s.lastTsAtEnd,
545
+ seenMessageKeys: [],
546
+ codexTotals: { ...s.prevAtEnd }
547
+ }
548
+ },
549
+ endTs: s.endTs
550
+ }));
551
+ }
552
+
553
+ // src/transcript.ts
554
+ import { readFile as readFile3 } from "fs/promises";
555
+ var NEWLINE2 = 10;
556
+ var MAX_SEEN_KEYS = 500;
557
+ var SYNTHETIC_MODEL = "<synthetic>";
558
+ function isRecord2(v) {
559
+ return typeof v === "object" && v !== null && !Array.isArray(v);
560
+ }
561
+ function numOf2(v) {
562
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
563
+ }
564
+ function strOrNull2(v) {
565
+ return typeof v === "string" ? v : null;
566
+ }
294
567
  function emptyBuckets() {
295
568
  return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
296
569
  }
@@ -304,17 +577,17 @@ function addToModel(target, model, b) {
304
577
  target[model] = cur;
305
578
  }
306
579
  function extractBucket(usage) {
307
- const input = numOf(usage.input_tokens);
308
- const output = numOf(usage.output_tokens);
309
- const cacheRead = numOf(usage.cache_read_input_tokens);
580
+ const input = numOf2(usage.input_tokens);
581
+ const output = numOf2(usage.output_tokens);
582
+ const cacheRead = numOf2(usage.cache_read_input_tokens);
310
583
  let cacheWrite5m;
311
584
  let cacheWrite1h;
312
585
  const cc = usage.cache_creation;
313
- if (isRecord(cc)) {
314
- cacheWrite5m = numOf(cc.ephemeral_5m_input_tokens);
315
- cacheWrite1h = numOf(cc.ephemeral_1h_input_tokens);
586
+ if (isRecord2(cc)) {
587
+ cacheWrite5m = numOf2(cc.ephemeral_5m_input_tokens);
588
+ cacheWrite1h = numOf2(cc.ephemeral_1h_input_tokens);
316
589
  } else {
317
- cacheWrite5m = numOf(usage.cache_creation_input_tokens);
590
+ cacheWrite5m = numOf2(usage.cache_creation_input_tokens);
318
591
  cacheWrite1h = 0;
319
592
  }
320
593
  return { input, output, cacheWrite5m, cacheWrite1h, cacheRead };
@@ -325,7 +598,7 @@ function promptCandidate(content) {
325
598
  let hasToolResult = false;
326
599
  const texts = [];
327
600
  for (const block of content) {
328
- if (!isRecord(block)) continue;
601
+ if (!isRecord2(block)) continue;
329
602
  if (block.type === "tool_result") hasToolResult = true;
330
603
  else if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
331
604
  }
@@ -334,20 +607,20 @@ function promptCandidate(content) {
334
607
  }
335
608
  return null;
336
609
  }
337
- async function readAll(path2) {
610
+ async function readAll2(path2) {
338
611
  try {
339
- return await readFile2(path2);
612
+ return await readFile3(path2);
340
613
  } catch {
341
614
  return null;
342
615
  }
343
616
  }
344
617
  async function aggregateNewTurn(transcriptPath, cursor) {
345
- const buffer = await readAll(transcriptPath);
618
+ const buffer = await readAll2(transcriptPath);
346
619
  if (buffer === null) return null;
347
620
  const fileSize = buffer.length;
348
621
  let startOffset;
349
622
  let rescan;
350
- if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
623
+ if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE2) {
351
624
  startOffset = cursor.offset;
352
625
  rescan = false;
353
626
  } else {
@@ -372,26 +645,26 @@ async function aggregateNewTurn(transcriptPath, cursor) {
372
645
  } catch {
373
646
  return;
374
647
  }
375
- if (!isRecord(obj)) return;
376
- const ts = strOrNull(obj.timestamp);
648
+ if (!isRecord2(obj)) return;
649
+ const ts = strOrNull2(obj.timestamp);
377
650
  if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
378
651
  const isSide = obj.isSidechain === true;
379
- const sid = strOrNull(obj.sessionId);
652
+ const sid = strOrNull2(obj.sessionId);
380
653
  if (sid !== null) sessionId = sid;
381
654
  if (!isSide) {
382
- const c = strOrNull(obj.cwd);
655
+ const c = strOrNull2(obj.cwd);
383
656
  if (c !== null) cwd = c;
384
- const gb = strOrNull(obj.gitBranch);
657
+ const gb = strOrNull2(obj.gitBranch);
385
658
  if (gb !== null) gitBranch = gb;
386
659
  }
387
660
  if (ts !== null) {
388
661
  if (firstTs === null || ts < firstTs) firstTs = ts;
389
662
  if (lastTs === null || ts > lastTs) lastTs = ts;
390
663
  }
391
- const uuid = strOrNull(obj.uuid);
664
+ const uuid = strOrNull2(obj.uuid);
392
665
  if (uuid !== null) lastUuid = uuid;
393
666
  const type = obj.type;
394
- const message = isRecord(obj.message) ? obj.message : null;
667
+ const message = isRecord2(obj.message) ? obj.message : null;
395
668
  if (type === "user" && !isSide && message !== null) {
396
669
  const cand = promptCandidate(message.content);
397
670
  if (cand !== null) {
@@ -401,14 +674,14 @@ async function aggregateNewTurn(transcriptPath, cursor) {
401
674
  }
402
675
  if (type === "assistant" && message !== null) {
403
676
  const usage = message.usage;
404
- if (isRecord(usage)) {
677
+ if (isRecord2(usage)) {
405
678
  const rawModel = message.model;
406
679
  if (rawModel !== SYNTHETIC_MODEL) {
407
- const id = strOrNull(message.id) ?? "";
408
- const reqId = strOrNull(obj.requestId) ?? "";
680
+ const id = strOrNull2(message.id) ?? "";
681
+ const reqId = strOrNull2(obj.requestId) ?? "";
409
682
  const key = `${id}:${reqId}`;
410
683
  if (!seenKeys.has(key)) {
411
- const model = strOrNull(rawModel) ?? "unknown";
684
+ const model = strOrNull2(rawModel) ?? "unknown";
412
685
  pending.set(key, { model, isSidechain: isSide, bucket: extractBucket(usage) });
413
686
  }
414
687
  }
@@ -417,7 +690,7 @@ async function aggregateNewTurn(transcriptPath, cursor) {
417
690
  };
418
691
  let lineStart = startOffset;
419
692
  for (let pos = startOffset; pos < fileSize; pos++) {
420
- if (buffer[pos] !== NEWLINE) continue;
693
+ if (buffer[pos] !== NEWLINE2) continue;
421
694
  handleLine(buffer.toString("utf8", lineStart, pos));
422
695
  lineStart = pos + 1;
423
696
  }
@@ -456,6 +729,8 @@ export {
456
729
  computeCost,
457
730
  loadPriceTable,
458
731
  getUsdJpy,
732
+ aggregateCodexTurn,
733
+ splitIntoCodexTurnDrafts,
459
734
  extractBucket,
460
735
  promptCandidate,
461
736
  aggregateNewTurn
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ aggregateNewTurn
4
+ } from "./chunk-LVMKY6JB.js";
5
+ import {
6
+ loadCursor,
7
+ logError,
8
+ sanitizeCursor
9
+ } from "./chunk-5PH7PPD6.js";
10
+
11
+ // src/subagents.ts
12
+ import { promises as fs } from "fs";
13
+ import { join } from "path";
14
+ var MAX_AGENT_FILES = 200;
15
+ function emptyBuckets() {
16
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
17
+ }
18
+ function addToModel(target, model, b) {
19
+ const cur = target[model] ?? emptyBuckets();
20
+ cur.input += b.input;
21
+ cur.output += b.output;
22
+ cur.cacheWrite5m += b.cacheWrite5m;
23
+ cur.cacheWrite1h += b.cacheWrite1h;
24
+ cur.cacheRead += b.cacheRead;
25
+ target[model] = cur;
26
+ }
27
+ function mergeUsage(target, src) {
28
+ for (const [model, b] of Object.entries(src)) addToModel(target, model, b);
29
+ }
30
+ function subagentsDirOf(mainTranscriptPath) {
31
+ const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
32
+ return join(base, "subagents");
33
+ }
34
+ async function listAgentFiles(dir, entries) {
35
+ const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
36
+ if (files.length <= MAX_AGENT_FILES) return files;
37
+ const withMtime = [];
38
+ for (const p of files) {
39
+ let mtime = 0;
40
+ try {
41
+ mtime = (await fs.stat(p)).mtimeMs;
42
+ } catch {
43
+ mtime = 0;
44
+ }
45
+ withMtime.push({ path: p, mtime });
46
+ }
47
+ withMtime.sort((a, b) => b.mtime - a.mtime);
48
+ return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
49
+ }
50
+ async function collectSubagentUsage(mainTranscriptPath) {
51
+ const dir = subagentsDirOf(mainTranscriptPath);
52
+ let entries;
53
+ try {
54
+ entries = await fs.readdir(dir, { withFileTypes: true });
55
+ } catch {
56
+ return null;
57
+ }
58
+ const files = await listAgentFiles(dir, entries);
59
+ const perModel = {};
60
+ let apiCalls = 0;
61
+ let agentFiles = 0;
62
+ const newCursors = [];
63
+ for (const filePath of files) {
64
+ try {
65
+ const cursor = sanitizeCursor(loadCursor(filePath));
66
+ const agg = await aggregateNewTurn(filePath, cursor);
67
+ if (agg === null) continue;
68
+ mergeUsage(perModel, agg.main);
69
+ mergeUsage(perModel, agg.sidechain);
70
+ apiCalls += agg.apiCalls;
71
+ agentFiles += 1;
72
+ newCursors.push({ path: filePath, cursor: agg.newCursor });
73
+ } catch (err) {
74
+ logError("subagents:file", err);
75
+ }
76
+ }
77
+ return { perModel, apiCalls, agentFiles, newCursors };
78
+ }
79
+
80
+ export {
81
+ collectSubagentUsage
82
+ };