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,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
notifyOS,
|
|
4
|
+
notifySlack
|
|
5
|
+
} from "./chunk-NV5UOHJA.js";
|
|
6
|
+
import {
|
|
7
|
+
writeDashboardHtml
|
|
8
|
+
} from "./chunk-QWBQN4XY.js";
|
|
9
|
+
import "./chunk-DGXUSPS4.js";
|
|
10
|
+
import {
|
|
11
|
+
collectSubagentUsage
|
|
12
|
+
} from "./chunk-O2AR4PXU.js";
|
|
13
|
+
import {
|
|
14
|
+
aggregateCodexTurn,
|
|
15
|
+
aggregateNewTurn,
|
|
16
|
+
computeCost,
|
|
17
|
+
getUsdJpy,
|
|
18
|
+
loadPriceTable
|
|
19
|
+
} from "./chunk-LVMKY6JB.js";
|
|
20
|
+
import "./chunk-J5QAYTFE.js";
|
|
21
|
+
import {
|
|
22
|
+
isFullDashboardDue,
|
|
23
|
+
makeFullDashboardState,
|
|
24
|
+
writeFullDashboardStateAtomic
|
|
25
|
+
} from "./chunk-ENGUOLTD.js";
|
|
26
|
+
import {
|
|
27
|
+
waitForDataLock
|
|
28
|
+
} from "./chunk-O34L3NSI.js";
|
|
29
|
+
import {
|
|
30
|
+
appendTurn,
|
|
31
|
+
closeCodexRootContext,
|
|
32
|
+
isMuted,
|
|
33
|
+
loadCursor,
|
|
34
|
+
logError,
|
|
35
|
+
paths,
|
|
36
|
+
readConfig,
|
|
37
|
+
readTurns,
|
|
38
|
+
sanitizeCursor,
|
|
39
|
+
saveCursor,
|
|
40
|
+
todayTotalUSD
|
|
41
|
+
} from "./chunk-5PH7PPD6.js";
|
|
42
|
+
|
|
43
|
+
// src/track.ts
|
|
44
|
+
function isRecord(v) {
|
|
45
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
46
|
+
}
|
|
47
|
+
function emptyBuckets() {
|
|
48
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
49
|
+
}
|
|
50
|
+
function sumBuckets(usage) {
|
|
51
|
+
const total = emptyBuckets();
|
|
52
|
+
for (const b of Object.values(usage)) {
|
|
53
|
+
total.input += b.input;
|
|
54
|
+
total.output += b.output;
|
|
55
|
+
total.cacheWrite5m += b.cacheWrite5m;
|
|
56
|
+
total.cacheWrite1h += b.cacheWrite1h;
|
|
57
|
+
total.cacheRead += b.cacheRead;
|
|
58
|
+
}
|
|
59
|
+
return total;
|
|
60
|
+
}
|
|
61
|
+
function collectModels(main, sidechain) {
|
|
62
|
+
const models = [];
|
|
63
|
+
for (const m of Object.keys(main)) {
|
|
64
|
+
if (!models.includes(m)) models.push(m);
|
|
65
|
+
}
|
|
66
|
+
for (const m of Object.keys(sidechain)) {
|
|
67
|
+
if (!models.includes(m)) models.push(m);
|
|
68
|
+
}
|
|
69
|
+
return models;
|
|
70
|
+
}
|
|
71
|
+
function withCodexModel(agg, payloadModel) {
|
|
72
|
+
const model = typeof payloadModel === "string" && payloadModel.length > 0 ? payloadModel : null;
|
|
73
|
+
if (model === null) return agg;
|
|
74
|
+
const buckets = Object.values(agg.main)[0] ?? emptyBuckets();
|
|
75
|
+
return { ...agg, main: { [model]: buckets } };
|
|
76
|
+
}
|
|
77
|
+
async function runTrack(stdinText, opts) {
|
|
78
|
+
try {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(stdinText);
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(parsed)) return;
|
|
86
|
+
const input = parsed;
|
|
87
|
+
const isCodex = opts?.codex === true;
|
|
88
|
+
let activityProjectionKey = null;
|
|
89
|
+
if (isCodex) {
|
|
90
|
+
try {
|
|
91
|
+
activityProjectionKey = closeCodexRootContext(parsed);
|
|
92
|
+
} catch {
|
|
93
|
+
logError("track:codex-subagent-projection", new Error("activity projection was not attached"));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const transcriptPath = input.transcript_path;
|
|
97
|
+
if (typeof transcriptPath !== "string") return;
|
|
98
|
+
const cfg = readConfig();
|
|
99
|
+
const cacheDir = paths().cacheDir;
|
|
100
|
+
const table = await loadPriceTable(cacheDir, { offline: true });
|
|
101
|
+
const fx = await getUsdJpy(cfg, cacheDir);
|
|
102
|
+
let record;
|
|
103
|
+
const commitLock = await waitForDataLock(1e3);
|
|
104
|
+
if (commitLock === null) {
|
|
105
|
+
logError("track:data-lock", new Error("data lock timeout; turn was not consumed"));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const cursor = sanitizeCursor(loadCursor(transcriptPath));
|
|
110
|
+
let agg = isCodex ? await aggregateCodexTurn(transcriptPath, cursor) : await aggregateNewTurn(transcriptPath, cursor);
|
|
111
|
+
if (agg === null) return;
|
|
112
|
+
if (isCodex) {
|
|
113
|
+
agg = withCodexModel(agg, input.model);
|
|
114
|
+
}
|
|
115
|
+
let sa = null;
|
|
116
|
+
if (!isCodex) {
|
|
117
|
+
try {
|
|
118
|
+
sa = await collectSubagentUsage(transcriptPath);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logError("track:subagents", err);
|
|
121
|
+
sa = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const breakdown = computeCost(agg.main, agg.sidechain, table);
|
|
125
|
+
const sessionId = agg.sessionId || (typeof input.session_id === "string" ? input.session_id : "") || "";
|
|
126
|
+
const project = agg.cwd ?? (typeof input.cwd === "string" ? input.cwd : void 0) ?? "";
|
|
127
|
+
const sidechainHasModels = Object.keys(agg.sidechain).length > 0;
|
|
128
|
+
record = {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
sessionId,
|
|
132
|
+
project,
|
|
133
|
+
gitBranch: agg.gitBranch,
|
|
134
|
+
models: collectModels(agg.main, agg.sidechain),
|
|
135
|
+
tokens: sumBuckets(agg.main),
|
|
136
|
+
sidechainTokens: sidechainHasModels ? sumBuckets(agg.sidechain) : null,
|
|
137
|
+
apiCalls: agg.apiCalls,
|
|
138
|
+
costUSD: breakdown.usd,
|
|
139
|
+
costByModel: breakdown.byModel,
|
|
140
|
+
// モデル別 USD(main+sidechain 合算、丸めない)
|
|
141
|
+
costJPY: breakdown.usd * fx.rate,
|
|
142
|
+
// 丸めない(表示時に丸める)
|
|
143
|
+
fxRate: fx.rate,
|
|
144
|
+
fxSource: fx.source,
|
|
145
|
+
prompt: agg.prompt ?? ""
|
|
146
|
+
};
|
|
147
|
+
if (isCodex) {
|
|
148
|
+
record.source = "codex";
|
|
149
|
+
if (activityProjectionKey !== null) record.activityProjectionKey = activityProjectionKey;
|
|
150
|
+
}
|
|
151
|
+
if (breakdown.unknownModels.length > 0) {
|
|
152
|
+
record.unknownModels = breakdown.unknownModels;
|
|
153
|
+
}
|
|
154
|
+
if (sa !== null && sa.apiCalls > 0) {
|
|
155
|
+
const saBreakdown = computeCost(sa.perModel, {}, table);
|
|
156
|
+
record.subagents = {
|
|
157
|
+
costUSD: saBreakdown.usd,
|
|
158
|
+
costByModel: saBreakdown.byModel,
|
|
159
|
+
tokens: sumBuckets(sa.perModel),
|
|
160
|
+
apiCalls: sa.apiCalls,
|
|
161
|
+
agentFiles: sa.agentFiles
|
|
162
|
+
};
|
|
163
|
+
if (saBreakdown.unknownModels.length > 0) {
|
|
164
|
+
const merged = record.unknownModels ? [...record.unknownModels] : [];
|
|
165
|
+
for (const m of saBreakdown.unknownModels) {
|
|
166
|
+
if (!merged.includes(m)) merged.push(m);
|
|
167
|
+
}
|
|
168
|
+
record.unknownModels = merged;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
appendTurn(record);
|
|
172
|
+
saveCursor(transcriptPath, agg.newCursor);
|
|
173
|
+
if (sa !== null) {
|
|
174
|
+
for (const nc of sa.newCursors) {
|
|
175
|
+
saveCursor(nc.path, nc.cursor);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
commitLock.release();
|
|
180
|
+
}
|
|
181
|
+
const tasks = [];
|
|
182
|
+
if ((cfg.notify.os || cfg.notify.slack !== null) && record.costUSD >= cfg.minNotifyUSD && !isMuted()) {
|
|
183
|
+
const todayUSD = cfg.includeDailyTotal ? todayTotalUSD() : void 0;
|
|
184
|
+
tasks.push(notifyOS(record, cfg, todayUSD));
|
|
185
|
+
tasks.push(notifySlack(record, cfg, todayUSD));
|
|
186
|
+
}
|
|
187
|
+
if (cfg.dashboard.autoRegenerate) {
|
|
188
|
+
tasks.push(
|
|
189
|
+
(async () => {
|
|
190
|
+
const now = /* @__PURE__ */ new Date();
|
|
191
|
+
const dashboardLock = await waitForDataLock(1e3);
|
|
192
|
+
if (dashboardLock === null) {
|
|
193
|
+
logError("track:dashboard-lock", new Error("data lock timeout; dashboard skipped"));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
let allTurns;
|
|
198
|
+
try {
|
|
199
|
+
allTurns = readTurns();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logError("track:dashboard-read", err);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
writeDashboardHtml({
|
|
206
|
+
days: cfg.dashboard.days,
|
|
207
|
+
outPath: paths().recentDashboardFile,
|
|
208
|
+
autoReloadSec: cfg.dashboard.autoReloadSec,
|
|
209
|
+
allTurns,
|
|
210
|
+
variant: "recent"
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
logError("track:dashboard-recent", err);
|
|
214
|
+
}
|
|
215
|
+
if (isFullDashboardDue(now)) {
|
|
216
|
+
try {
|
|
217
|
+
writeDashboardHtml({
|
|
218
|
+
days: null,
|
|
219
|
+
outPath: paths().fullDashboardFile,
|
|
220
|
+
autoReloadSec: cfg.dashboard.autoReloadSec,
|
|
221
|
+
allTurns,
|
|
222
|
+
variant: "full",
|
|
223
|
+
generatedAt: now.toISOString()
|
|
224
|
+
});
|
|
225
|
+
writeFullDashboardStateAtomic(makeFullDashboardState(now));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logError("track:dashboard-full", err);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} finally {
|
|
231
|
+
dashboardLock.release();
|
|
232
|
+
}
|
|
233
|
+
})()
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (tasks.length > 0) {
|
|
237
|
+
await Promise.allSettled(tasks);
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
logError("track", err);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
runTrack
|
|
245
|
+
};
|
package/package.json
CHANGED
package/dist/chunk-DSV75EF7.js
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
aggregateNewTurn
|
|
4
|
-
} from "./chunk-LHKBGA5K.js";
|
|
5
|
-
import {
|
|
6
|
-
loadCursor,
|
|
7
|
-
logError,
|
|
8
|
-
sanitizeCursor
|
|
9
|
-
} from "./chunk-ECADO26T.js";
|
|
10
|
-
|
|
11
|
-
// src/codex/transcript.ts
|
|
12
|
-
import { readFile } from "fs/promises";
|
|
13
|
-
import { basename } from "path";
|
|
14
|
-
var NEWLINE = 10;
|
|
15
|
-
function isRecord(v) {
|
|
16
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17
|
-
}
|
|
18
|
-
function numOf(v) {
|
|
19
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
20
|
-
}
|
|
21
|
-
function strOrNull(v) {
|
|
22
|
-
return typeof v === "string" ? v : null;
|
|
23
|
-
}
|
|
24
|
-
function zeroTotals() {
|
|
25
|
-
return { input: 0, cached: 0, output: 0 };
|
|
26
|
-
}
|
|
27
|
-
function isZeroTotals(t) {
|
|
28
|
-
return t.input === 0 && t.cached === 0 && t.output === 0;
|
|
29
|
-
}
|
|
30
|
-
function addTotals(target, d) {
|
|
31
|
-
target.input += d.input;
|
|
32
|
-
target.cached += d.cached;
|
|
33
|
-
target.output += d.output;
|
|
34
|
-
}
|
|
35
|
-
function readTotals(v) {
|
|
36
|
-
if (!isRecord(v)) return null;
|
|
37
|
-
return {
|
|
38
|
-
input: numOf(v.input_tokens),
|
|
39
|
-
cached: numOf(v.cached_input_tokens),
|
|
40
|
-
output: numOf(v.output_tokens)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function totalsToBuckets(acc) {
|
|
44
|
-
return {
|
|
45
|
-
input: Math.max(0, acc.input - acc.cached),
|
|
46
|
-
output: acc.output,
|
|
47
|
-
cacheWrite5m: 0,
|
|
48
|
-
cacheWrite1h: 0,
|
|
49
|
-
cacheRead: acc.cached
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function sessionIdFromFilename(rolloutPath) {
|
|
53
|
-
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(
|
|
54
|
-
basename(rolloutPath)
|
|
55
|
-
);
|
|
56
|
-
return m !== null ? m[1] : "";
|
|
57
|
-
}
|
|
58
|
-
async function readAll(path) {
|
|
59
|
-
try {
|
|
60
|
-
return await readFile(path);
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function newSegmentBuf() {
|
|
66
|
-
return {
|
|
67
|
-
acc: zeroTotals(),
|
|
68
|
-
apiCalls: 0,
|
|
69
|
-
prompt: null,
|
|
70
|
-
turnCtxCwd: null,
|
|
71
|
-
firstTs: null,
|
|
72
|
-
endTs: null,
|
|
73
|
-
hasLines: false
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
async function scanWindow(rolloutPath, cursor) {
|
|
77
|
-
const buffer = await readAll(rolloutPath);
|
|
78
|
-
if (buffer === null) return null;
|
|
79
|
-
const fileSize = buffer.length;
|
|
80
|
-
let startOffset;
|
|
81
|
-
let rescan;
|
|
82
|
-
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
|
|
83
|
-
startOffset = cursor.offset;
|
|
84
|
-
rescan = false;
|
|
85
|
-
} else {
|
|
86
|
-
startOffset = 0;
|
|
87
|
-
rescan = cursor !== null;
|
|
88
|
-
}
|
|
89
|
-
const tsFloor = cursor?.lastTs ?? null;
|
|
90
|
-
const initTotals = cursor?.codexTotals;
|
|
91
|
-
let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
|
|
92
|
-
const acc = zeroTotals();
|
|
93
|
-
let apiCalls = 0;
|
|
94
|
-
let lastModel = null;
|
|
95
|
-
let windowPrompt = null;
|
|
96
|
-
let windowTurnCtxCwd = null;
|
|
97
|
-
let sessionMetaCwd = null;
|
|
98
|
-
let sessionMetaSid = null;
|
|
99
|
-
let firstTs = null;
|
|
100
|
-
let lastTs = null;
|
|
101
|
-
const segments = [];
|
|
102
|
-
let seg = newSegmentBuf();
|
|
103
|
-
const snapshotSegment = (endOffset) => ({
|
|
104
|
-
acc: seg.acc,
|
|
105
|
-
apiCalls: seg.apiCalls,
|
|
106
|
-
prompt: seg.prompt,
|
|
107
|
-
model: lastModel,
|
|
108
|
-
cwd: seg.turnCtxCwd ?? sessionMetaCwd,
|
|
109
|
-
firstTs: seg.firstTs,
|
|
110
|
-
endTs: seg.endTs,
|
|
111
|
-
endOffset,
|
|
112
|
-
prevAtEnd: { ...prev },
|
|
113
|
-
lastTsAtEnd: lastTs
|
|
114
|
-
});
|
|
115
|
-
const handleLine = (raw, endOffset) => {
|
|
116
|
-
if (raw.trim().length === 0) return;
|
|
117
|
-
let obj;
|
|
118
|
-
try {
|
|
119
|
-
obj = JSON.parse(raw);
|
|
120
|
-
} catch {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (!isRecord(obj)) return;
|
|
124
|
-
const ts = strOrNull(obj.timestamp);
|
|
125
|
-
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
126
|
-
if (ts !== null) {
|
|
127
|
-
if (firstTs === null || ts < firstTs) firstTs = ts;
|
|
128
|
-
if (lastTs === null || ts > lastTs) lastTs = ts;
|
|
129
|
-
if (seg.firstTs === null || ts < seg.firstTs) seg.firstTs = ts;
|
|
130
|
-
seg.endTs = ts;
|
|
131
|
-
}
|
|
132
|
-
seg.hasLines = true;
|
|
133
|
-
const payload = isRecord(obj.payload) ? obj.payload : null;
|
|
134
|
-
if (payload === null) return;
|
|
135
|
-
const type = obj.type;
|
|
136
|
-
if (type === "session_meta") {
|
|
137
|
-
const sid = strOrNull(payload.session_id);
|
|
138
|
-
if (sid !== null) sessionMetaSid = sid;
|
|
139
|
-
const c = strOrNull(payload.cwd);
|
|
140
|
-
if (c !== null) sessionMetaCwd = c;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (type === "turn_context") {
|
|
144
|
-
const m = strOrNull(payload.model);
|
|
145
|
-
if (m !== null) lastModel = m;
|
|
146
|
-
const c = strOrNull(payload.cwd);
|
|
147
|
-
if (c !== null) {
|
|
148
|
-
seg.turnCtxCwd = c;
|
|
149
|
-
windowTurnCtxCwd = c;
|
|
150
|
-
}
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (type !== "event_msg") return;
|
|
154
|
-
const kind = payload.type;
|
|
155
|
-
if (kind === "user_message") {
|
|
156
|
-
const msg = strOrNull(payload.message);
|
|
157
|
-
if (msg !== null) {
|
|
158
|
-
seg.prompt = msg;
|
|
159
|
-
windowPrompt = msg;
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (kind === "token_count") {
|
|
164
|
-
const info = isRecord(payload.info) ? payload.info : null;
|
|
165
|
-
if (info === null) return;
|
|
166
|
-
const total = readTotals(info.total_token_usage);
|
|
167
|
-
if (total === null) return;
|
|
168
|
-
let step = {
|
|
169
|
-
input: total.input - prev.input,
|
|
170
|
-
cached: total.cached - prev.cached,
|
|
171
|
-
output: total.output - prev.output
|
|
172
|
-
};
|
|
173
|
-
if (step.input < 0 || step.cached < 0 || step.output < 0) {
|
|
174
|
-
step = readTotals(info.last_token_usage) ?? zeroTotals();
|
|
175
|
-
}
|
|
176
|
-
addTotals(acc, step);
|
|
177
|
-
addTotals(seg.acc, step);
|
|
178
|
-
prev = total;
|
|
179
|
-
if (!isZeroTotals(step)) {
|
|
180
|
-
apiCalls++;
|
|
181
|
-
seg.apiCalls++;
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (kind === "task_complete") {
|
|
186
|
-
segments.push(snapshotSegment(endOffset));
|
|
187
|
-
seg = newSegmentBuf();
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
let lineStart = startOffset;
|
|
191
|
-
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
192
|
-
if (buffer[pos] !== NEWLINE) continue;
|
|
193
|
-
handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
|
|
194
|
-
lineStart = pos + 1;
|
|
195
|
-
}
|
|
196
|
-
const newOffset = lineStart;
|
|
197
|
-
const open = seg.hasLines ? snapshotSegment(newOffset) : null;
|
|
198
|
-
return {
|
|
199
|
-
segments,
|
|
200
|
-
open,
|
|
201
|
-
acc,
|
|
202
|
-
prev,
|
|
203
|
-
apiCalls,
|
|
204
|
-
model: lastModel,
|
|
205
|
-
prompt: windowPrompt,
|
|
206
|
-
cwd: windowTurnCtxCwd ?? sessionMetaCwd,
|
|
207
|
-
sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
|
|
208
|
-
firstTs,
|
|
209
|
-
lastTs,
|
|
210
|
-
newOffset
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function windowCursor(scan) {
|
|
214
|
-
return {
|
|
215
|
-
offset: scan.newOffset,
|
|
216
|
-
lastUuid: null,
|
|
217
|
-
// rollout に uuid 行は無い
|
|
218
|
-
lastTs: scan.lastTs,
|
|
219
|
-
seenMessageKeys: [],
|
|
220
|
-
// 去重は codexTotals の差分方式が担う
|
|
221
|
-
codexTotals: { ...scan.prev }
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
async function aggregateCodexTurn(rolloutPath, cursor) {
|
|
225
|
-
const scan = await scanWindow(rolloutPath, cursor);
|
|
226
|
-
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
227
|
-
return {
|
|
228
|
-
sessionId: scan.sessionId,
|
|
229
|
-
main: { [scan.model ?? "unknown"]: totalsToBuckets(scan.acc) },
|
|
230
|
-
sidechain: {},
|
|
231
|
-
// Codex にサブエージェント概念は無い
|
|
232
|
-
apiCalls: scan.apiCalls,
|
|
233
|
-
prompt: scan.prompt,
|
|
234
|
-
cwd: scan.cwd,
|
|
235
|
-
gitBranch: null,
|
|
236
|
-
// rollout に無い
|
|
237
|
-
firstTs: scan.firstTs,
|
|
238
|
-
lastTs: scan.lastTs,
|
|
239
|
-
newCursor: windowCursor(scan)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
243
|
-
const scan = await scanWindow(rolloutPath, cursor);
|
|
244
|
-
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
245
|
-
const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
|
|
246
|
-
if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
|
|
247
|
-
const last = picked[picked.length - 1];
|
|
248
|
-
if (last !== void 0) {
|
|
249
|
-
addTotals(last.acc, scan.open.acc);
|
|
250
|
-
last.apiCalls += scan.open.apiCalls;
|
|
251
|
-
if (scan.open.endTs !== null) last.endTs = scan.open.endTs;
|
|
252
|
-
} else {
|
|
253
|
-
picked.push(scan.open);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const lastIndex = picked.length - 1;
|
|
257
|
-
return picked.map((s, i) => ({
|
|
258
|
-
agg: {
|
|
259
|
-
sessionId: scan.sessionId,
|
|
260
|
-
// session_meta はファイル先頭にしか無いので全ドラフト共通
|
|
261
|
-
main: { [s.model ?? "unknown"]: totalsToBuckets(s.acc) },
|
|
262
|
-
sidechain: {},
|
|
263
|
-
apiCalls: s.apiCalls,
|
|
264
|
-
prompt: s.prompt,
|
|
265
|
-
cwd: s.cwd,
|
|
266
|
-
gitBranch: null,
|
|
267
|
-
firstTs: s.firstTs,
|
|
268
|
-
lastTs: s.endTs,
|
|
269
|
-
// 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
|
|
270
|
-
// 末尾の usage ゼロな行の読み捨てもここに含まれる)。途中のドラフトはそのセグメント末尾を
|
|
271
|
-
// 指す有効な再開点(そこから読み直せば残りが差分になる)。
|
|
272
|
-
newCursor: i === lastIndex ? windowCursor(scan) : {
|
|
273
|
-
offset: s.endOffset,
|
|
274
|
-
lastUuid: null,
|
|
275
|
-
lastTs: s.lastTsAtEnd,
|
|
276
|
-
seenMessageKeys: [],
|
|
277
|
-
codexTotals: { ...s.prevAtEnd }
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
endTs: s.endTs
|
|
281
|
-
}));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/subagents.ts
|
|
285
|
-
import { promises as fs } from "fs";
|
|
286
|
-
import { join } from "path";
|
|
287
|
-
var MAX_AGENT_FILES = 200;
|
|
288
|
-
function emptyBuckets() {
|
|
289
|
-
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
290
|
-
}
|
|
291
|
-
function addToModel(target, model, b) {
|
|
292
|
-
const cur = target[model] ?? emptyBuckets();
|
|
293
|
-
cur.input += b.input;
|
|
294
|
-
cur.output += b.output;
|
|
295
|
-
cur.cacheWrite5m += b.cacheWrite5m;
|
|
296
|
-
cur.cacheWrite1h += b.cacheWrite1h;
|
|
297
|
-
cur.cacheRead += b.cacheRead;
|
|
298
|
-
target[model] = cur;
|
|
299
|
-
}
|
|
300
|
-
function mergeUsage(target, src) {
|
|
301
|
-
for (const [model, b] of Object.entries(src)) addToModel(target, model, b);
|
|
302
|
-
}
|
|
303
|
-
function subagentsDirOf(mainTranscriptPath) {
|
|
304
|
-
const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
|
|
305
|
-
return join(base, "subagents");
|
|
306
|
-
}
|
|
307
|
-
async function listAgentFiles(dir, entries) {
|
|
308
|
-
const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
|
|
309
|
-
if (files.length <= MAX_AGENT_FILES) return files;
|
|
310
|
-
const withMtime = [];
|
|
311
|
-
for (const p of files) {
|
|
312
|
-
let mtime = 0;
|
|
313
|
-
try {
|
|
314
|
-
mtime = (await fs.stat(p)).mtimeMs;
|
|
315
|
-
} catch {
|
|
316
|
-
mtime = 0;
|
|
317
|
-
}
|
|
318
|
-
withMtime.push({ path: p, mtime });
|
|
319
|
-
}
|
|
320
|
-
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
321
|
-
return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
|
|
322
|
-
}
|
|
323
|
-
async function collectSubagentUsage(mainTranscriptPath) {
|
|
324
|
-
const dir = subagentsDirOf(mainTranscriptPath);
|
|
325
|
-
let entries;
|
|
326
|
-
try {
|
|
327
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
328
|
-
} catch {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
const files = await listAgentFiles(dir, entries);
|
|
332
|
-
const perModel = {};
|
|
333
|
-
let apiCalls = 0;
|
|
334
|
-
let agentFiles = 0;
|
|
335
|
-
const newCursors = [];
|
|
336
|
-
for (const filePath of files) {
|
|
337
|
-
try {
|
|
338
|
-
const cursor = sanitizeCursor(loadCursor(filePath));
|
|
339
|
-
const agg = await aggregateNewTurn(filePath, cursor);
|
|
340
|
-
if (agg === null) continue;
|
|
341
|
-
mergeUsage(perModel, agg.main);
|
|
342
|
-
mergeUsage(perModel, agg.sidechain);
|
|
343
|
-
apiCalls += agg.apiCalls;
|
|
344
|
-
agentFiles += 1;
|
|
345
|
-
newCursors.push({ path: filePath, cursor: agg.newCursor });
|
|
346
|
-
} catch (err) {
|
|
347
|
-
logError("subagents:file", err);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return { perModel, apiCalls, agentFiles, newCursors };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export {
|
|
354
|
-
aggregateCodexTurn,
|
|
355
|
-
splitIntoCodexTurnDrafts,
|
|
356
|
-
collectSubagentUsage
|
|
357
|
-
};
|