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.
- package/README.md +11 -7
- package/dist/{budget-KKTFYAXH.js → budget-TBWVHUKA.js} +1 -1
- package/dist/chunk-5PH7PPD6.js +1077 -0
- package/dist/{chunk-4RZ6OGTD.js → chunk-DDUK4EWQ.js} +1 -1
- package/dist/chunk-ENGUOLTD.js +92 -0
- package/dist/chunk-HLJAJS2W.js +55 -0
- package/dist/{chunk-LHKBGA5K.js → chunk-LVMKY6JB.js} +304 -29
- package/dist/chunk-O2AR4PXU.js +82 -0
- package/dist/chunk-O34L3NSI.js +172 -0
- package/dist/{chunk-QX5KIRSU.js → chunk-QWBQN4XY.js} +183 -54
- package/dist/{chunk-7KVOQ4UZ.js → chunk-S3XKO7MY.js} +235 -108
- package/dist/cli.js +382 -73
- package/dist/{dashboard-TH3NOVT3.js → dashboard-6F2QNUJT.js} +4 -2
- package/dist/history-7KN26LJH.js +162 -0
- package/dist/{mute-EPLYH67P.js → mute-MPX3K7QN.js} +2 -2
- package/dist/{setup-PTO5N46B.js → setup-IXPMVWOE.js} +2 -2
- package/dist/subagent-store-A6MIN22X.js +31 -0
- package/dist/{sweep-25EZAOGC.js → sweep-UIONM4UA.js} +143 -126
- package/dist/track-KEZON6KI.js +245 -0
- package/package.json +1 -1
- package/dist/chunk-DSV75EF7.js +0 -357
- package/dist/chunk-ECADO26T.js +0 -316
- package/dist/history-DLFYQJFM.js +0 -114
- package/dist/track-PXFSGMGL.js +0 -184
|
@@ -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]
|
|
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 =
|
|
308
|
-
const output =
|
|
309
|
-
const cacheRead =
|
|
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 (
|
|
314
|
-
cacheWrite5m =
|
|
315
|
-
cacheWrite1h =
|
|
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 =
|
|
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 (!
|
|
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
|
|
610
|
+
async function readAll2(path2) {
|
|
338
611
|
try {
|
|
339
|
-
return await
|
|
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
|
|
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] ===
|
|
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 (!
|
|
376
|
-
const ts =
|
|
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 =
|
|
652
|
+
const sid = strOrNull2(obj.sessionId);
|
|
380
653
|
if (sid !== null) sessionId = sid;
|
|
381
654
|
if (!isSide) {
|
|
382
|
-
const c =
|
|
655
|
+
const c = strOrNull2(obj.cwd);
|
|
383
656
|
if (c !== null) cwd = c;
|
|
384
|
-
const gb =
|
|
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 =
|
|
664
|
+
const uuid = strOrNull2(obj.uuid);
|
|
392
665
|
if (uuid !== null) lastUuid = uuid;
|
|
393
666
|
const type = obj.type;
|
|
394
|
-
const message =
|
|
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 (
|
|
677
|
+
if (isRecord2(usage)) {
|
|
405
678
|
const rawModel = message.model;
|
|
406
679
|
if (rawModel !== SYNTHETIC_MODEL) {
|
|
407
|
-
const id =
|
|
408
|
-
const reqId =
|
|
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 =
|
|
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] !==
|
|
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
|
+
};
|