codetime-cli 0.5.0 → 0.7.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/bin/codetime.mjs +152 -41
- package/package.json +1 -1
package/bin/codetime.mjs
CHANGED
|
@@ -160,6 +160,7 @@ import { fileURLToPath } from "node:url";
|
|
|
160
160
|
// ../shared/src/index.ts
|
|
161
161
|
import { createHash } from "node:crypto";
|
|
162
162
|
var AGENT_TIME_SCHEMA_VERSION = "2026-04-29";
|
|
163
|
+
var AGENT_ROLLUP_SCHEMA_VERSION = 3;
|
|
163
164
|
var TELEMETRY_EVENT_TYPES = [
|
|
164
165
|
"session.started",
|
|
165
166
|
"session.ended",
|
|
@@ -973,11 +974,12 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
|
973
974
|
import path2 from "node:path";
|
|
974
975
|
|
|
975
976
|
// src/lib/constants.ts
|
|
976
|
-
var PACKAGE_VERSION = true ? "0.
|
|
977
|
+
var PACKAGE_VERSION = true ? "0.7.0" : "0.1.1";
|
|
977
978
|
var DEFAULT_API_URL = "https://codetime.dev";
|
|
978
979
|
var DEFAULT_BACKFILL_BATCH_SIZE = 50;
|
|
979
980
|
var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
|
|
980
981
|
var ROLLUP_BUCKET_MS = 15 * 60 * 1e3;
|
|
982
|
+
var TURN_GAP_CLAMP_MS = 5 * 60 * 1e3;
|
|
981
983
|
var DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS = 60;
|
|
982
984
|
|
|
983
985
|
// src/lib/fields.ts
|
|
@@ -1930,16 +1932,29 @@ function claudeUsageFromMessage(message) {
|
|
|
1930
1932
|
const outputTokens = numberField(usage, "output_tokens") || 0;
|
|
1931
1933
|
const cachedInputTokens = cacheCreationInputTokens + cacheReadInputTokens;
|
|
1932
1934
|
const totalInputTokens = inputTokens + cachedInputTokens;
|
|
1935
|
+
const cacheCreationSplit = claudeCacheCreationSplit(usage);
|
|
1933
1936
|
return {
|
|
1934
1937
|
tokensInput: totalInputTokens || void 0,
|
|
1935
1938
|
tokensCachedInput: cachedInputTokens || void 0,
|
|
1936
1939
|
tokensCacheCreationInput: cacheCreationInputTokens || void 0,
|
|
1940
|
+
tokensCacheCreation5mInput: cacheCreationSplit?.fiveMinute,
|
|
1941
|
+
tokensCacheCreation1hInput: cacheCreationSplit?.oneHour,
|
|
1937
1942
|
tokensCacheReadInput: cacheReadInputTokens || void 0,
|
|
1938
1943
|
tokensOutput: outputTokens || void 0,
|
|
1939
1944
|
tokensTotal: totalInputTokens + outputTokens || void 0,
|
|
1940
1945
|
modelCalls: 1
|
|
1941
1946
|
};
|
|
1942
1947
|
}
|
|
1948
|
+
function claudeCacheCreationSplit(usage) {
|
|
1949
|
+
const breakdown = usage.cache_creation;
|
|
1950
|
+
if (!isPlainObject(breakdown)) {
|
|
1951
|
+
return void 0;
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
fiveMinute: numberField(breakdown, "ephemeral_5m_input_tokens") || 0,
|
|
1955
|
+
oneHour: numberField(breakdown, "ephemeral_1h_input_tokens") || 0
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1943
1958
|
function claudeSubagentMetrics(toolResult, fallbackDurationMs) {
|
|
1944
1959
|
const usage = objectField(toolResult, "usage");
|
|
1945
1960
|
const inputTokens = numberField(usage, "input_tokens") || 0;
|
|
@@ -1948,6 +1963,7 @@ function claudeSubagentMetrics(toolResult, fallbackDurationMs) {
|
|
|
1948
1963
|
const outputTokens = numberField(usage, "output_tokens") || 0;
|
|
1949
1964
|
const cachedInputTokens = cacheCreationInputTokens + cacheReadInputTokens;
|
|
1950
1965
|
const totalInputTokens = inputTokens + cachedInputTokens;
|
|
1966
|
+
const cacheCreationSplit = claudeCacheCreationSplit(usage);
|
|
1951
1967
|
const durationMs = numberField(toolResult, "totalDurationMs") || fallbackDurationMs;
|
|
1952
1968
|
return {
|
|
1953
1969
|
durationMs,
|
|
@@ -1956,6 +1972,8 @@ function claudeSubagentMetrics(toolResult, fallbackDurationMs) {
|
|
|
1956
1972
|
tokensInput: totalInputTokens || void 0,
|
|
1957
1973
|
tokensCachedInput: cachedInputTokens || void 0,
|
|
1958
1974
|
tokensCacheCreationInput: cacheCreationInputTokens || void 0,
|
|
1975
|
+
tokensCacheCreation5mInput: cacheCreationSplit?.fiveMinute,
|
|
1976
|
+
tokensCacheCreation1hInput: cacheCreationSplit?.oneHour,
|
|
1959
1977
|
tokensCacheReadInput: cacheReadInputTokens || void 0,
|
|
1960
1978
|
tokensOutput: outputTokens || void 0,
|
|
1961
1979
|
tokensTotal: numberField(toolResult, "totalTokens") || totalInputTokens + outputTokens || void 0
|
|
@@ -2243,6 +2261,23 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2243
2261
|
let sessionMetaLocked = false;
|
|
2244
2262
|
let lastTokenUsageKey;
|
|
2245
2263
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2264
|
+
const turnLastEventAt = /* @__PURE__ */ new Map();
|
|
2265
|
+
const turnStartedAt = /* @__PURE__ */ new Map();
|
|
2266
|
+
const closedTurnIds = /* @__PURE__ */ new Set();
|
|
2267
|
+
const pushEvent = (event, refs) => {
|
|
2268
|
+
const turnId = event.turnId;
|
|
2269
|
+
if (turnId && event.ts) {
|
|
2270
|
+
if (!turnStartedAt.has(turnId)) {
|
|
2271
|
+
turnStartedAt.set(turnId, event.ts);
|
|
2272
|
+
}
|
|
2273
|
+
const prev = turnLastEventAt.get(turnId);
|
|
2274
|
+
if (!prev || event.ts > prev) {
|
|
2275
|
+
turnLastEventAt.set(turnId, event.ts);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
events.push(withBackfillRefs(event, refs));
|
|
2279
|
+
};
|
|
2280
|
+
const turnCloseTs = (turnId) => turnLastEventAt.get(turnId) || turnStartedAt.get(turnId) || (/* @__PURE__ */ new Date()).toISOString();
|
|
2246
2281
|
for (const [index, line] of lines.entries()) {
|
|
2247
2282
|
const lineNumber = index + 1;
|
|
2248
2283
|
const raw = parseJsonLine(line);
|
|
@@ -2299,23 +2334,23 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2299
2334
|
const eventModel = parsedModel || model || "gpt-5";
|
|
2300
2335
|
const headlessTs = headlessCodexTimestamp(raw) || ts;
|
|
2301
2336
|
if (!sessionStartEmitted) {
|
|
2302
|
-
|
|
2337
|
+
pushEvent(baseCodexEvent({
|
|
2303
2338
|
ts: headlessTs,
|
|
2304
2339
|
type: "session.started",
|
|
2305
2340
|
sessionId,
|
|
2306
2341
|
model: eventModel,
|
|
2307
2342
|
confidence: "derived"
|
|
2308
|
-
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "session", options })
|
|
2343
|
+
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "session", options });
|
|
2309
2344
|
sessionStartEmitted = true;
|
|
2310
2345
|
}
|
|
2311
|
-
|
|
2346
|
+
pushEvent(baseCodexEvent({
|
|
2312
2347
|
ts: headlessTs,
|
|
2313
2348
|
type: "model.usage",
|
|
2314
2349
|
sessionId,
|
|
2315
2350
|
model: eventModel,
|
|
2316
2351
|
confidence: "partial",
|
|
2317
2352
|
metrics: usage
|
|
2318
|
-
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "headless", options })
|
|
2353
|
+
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "headless", options });
|
|
2319
2354
|
}
|
|
2320
2355
|
continue;
|
|
2321
2356
|
}
|
|
@@ -2325,7 +2360,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2325
2360
|
switch (payloadType) {
|
|
2326
2361
|
case "task_started": {
|
|
2327
2362
|
currentTurnId = stringField(payload, "turn_id") || currentTurnId;
|
|
2328
|
-
|
|
2363
|
+
pushEvent(baseCodexEvent({
|
|
2329
2364
|
ts,
|
|
2330
2365
|
type: "turn.started",
|
|
2331
2366
|
sessionId,
|
|
@@ -2334,14 +2369,15 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2334
2369
|
project,
|
|
2335
2370
|
model,
|
|
2336
2371
|
confidence: "exact"
|
|
2337
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2372
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2338
2373
|
break;
|
|
2339
2374
|
}
|
|
2340
2375
|
case "user_message": {
|
|
2341
2376
|
const message = stringField(payload, "message") || "";
|
|
2342
|
-
if (currentTurnId && currentTurnId !== turnIdAtLastUserMessage && lastTurnIdForComplete) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2377
|
+
if (currentTurnId && currentTurnId !== turnIdAtLastUserMessage && lastTurnIdForComplete && !closedTurnIds.has(lastTurnIdForComplete)) {
|
|
2378
|
+
closedTurnIds.add(lastTurnIdForComplete);
|
|
2379
|
+
pushEvent(baseCodexEvent({
|
|
2380
|
+
ts: turnCloseTs(lastTurnIdForComplete),
|
|
2345
2381
|
type: "turn.completed",
|
|
2346
2382
|
sessionId,
|
|
2347
2383
|
turnId: lastTurnIdForComplete,
|
|
@@ -2349,14 +2385,14 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2349
2385
|
project,
|
|
2350
2386
|
model,
|
|
2351
2387
|
confidence: "derived"
|
|
2352
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2388
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2353
2389
|
}
|
|
2354
2390
|
if (!currentTurnId || currentTurnId === turnIdAtLastUserMessage) {
|
|
2355
2391
|
currentTurnId = `turn_${createStableHash([sessionId, lineNumber, ts]).slice(0, 24)}`;
|
|
2356
2392
|
}
|
|
2357
2393
|
lastTurnIdForComplete = currentTurnId;
|
|
2358
2394
|
turnIdAtLastUserMessage = currentTurnId;
|
|
2359
|
-
|
|
2395
|
+
pushEvent(baseCodexEvent({
|
|
2360
2396
|
ts,
|
|
2361
2397
|
type: "prompt.submitted",
|
|
2362
2398
|
sessionId,
|
|
@@ -2372,7 +2408,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2372
2408
|
refs: stringRefs({
|
|
2373
2409
|
promptHash: message ? `sha256:${createStableHash(message)}` : void 0
|
|
2374
2410
|
})
|
|
2375
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2411
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2376
2412
|
break;
|
|
2377
2413
|
}
|
|
2378
2414
|
case "token_count": {
|
|
@@ -2394,7 +2430,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2394
2430
|
break;
|
|
2395
2431
|
}
|
|
2396
2432
|
lastTokenUsageKey = usageKey;
|
|
2397
|
-
|
|
2433
|
+
pushEvent(baseCodexEvent({
|
|
2398
2434
|
ts,
|
|
2399
2435
|
type: "model.usage",
|
|
2400
2436
|
sessionId,
|
|
@@ -2407,13 +2443,13 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2407
2443
|
model: rewriteCodexModelForTier(model, serviceTier),
|
|
2408
2444
|
confidence: "partial",
|
|
2409
2445
|
metrics: usage
|
|
2410
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2446
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2411
2447
|
}
|
|
2412
2448
|
break;
|
|
2413
2449
|
}
|
|
2414
2450
|
case "agent_message": {
|
|
2415
2451
|
const message = stringField(payload, "message") || "";
|
|
2416
|
-
|
|
2452
|
+
pushEvent(baseCodexEvent({
|
|
2417
2453
|
ts,
|
|
2418
2454
|
type: "agent.operation",
|
|
2419
2455
|
operation: "agent message",
|
|
@@ -2426,7 +2462,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2426
2462
|
metrics: {
|
|
2427
2463
|
agentMessageChars: message.length
|
|
2428
2464
|
}
|
|
2429
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2465
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2430
2466
|
break;
|
|
2431
2467
|
}
|
|
2432
2468
|
case "function_call":
|
|
@@ -2438,7 +2474,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2438
2474
|
if (callId) {
|
|
2439
2475
|
pendingToolCalls.set(callId, { tool, startedAt: ts, turnId: currentTurnId });
|
|
2440
2476
|
}
|
|
2441
|
-
|
|
2477
|
+
pushEvent(baseCodexEvent({
|
|
2442
2478
|
ts,
|
|
2443
2479
|
type: "tool.started",
|
|
2444
2480
|
operation: `${tool} started`,
|
|
@@ -2455,9 +2491,9 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2455
2491
|
refs: stringRefs({
|
|
2456
2492
|
sourceId: callId
|
|
2457
2493
|
})
|
|
2458
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2494
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2459
2495
|
if (fileActivities.length > 0) {
|
|
2460
|
-
|
|
2496
|
+
pushEvent(baseCodexEvent({
|
|
2461
2497
|
ts,
|
|
2462
2498
|
type: eventTypeFromFileActivities(fileActivities),
|
|
2463
2499
|
operation: `${tool} file activity`,
|
|
@@ -2473,7 +2509,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2473
2509
|
refs: stringRefs({
|
|
2474
2510
|
sourceId: callId
|
|
2475
2511
|
})
|
|
2476
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2512
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2477
2513
|
}
|
|
2478
2514
|
break;
|
|
2479
2515
|
}
|
|
@@ -2485,7 +2521,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2485
2521
|
pendingToolCalls.delete(callId);
|
|
2486
2522
|
}
|
|
2487
2523
|
const durationMs = pending ? durationMsBetween(pending.startedAt, ts) : void 0;
|
|
2488
|
-
|
|
2524
|
+
pushEvent(baseCodexEvent({
|
|
2489
2525
|
ts,
|
|
2490
2526
|
type: "tool.completed",
|
|
2491
2527
|
operation: pending ? `${pending.tool} completed` : "tool completed",
|
|
@@ -2503,13 +2539,13 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2503
2539
|
refs: stringRefs({
|
|
2504
2540
|
sourceId: callId
|
|
2505
2541
|
})
|
|
2506
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2542
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2507
2543
|
break;
|
|
2508
2544
|
}
|
|
2509
2545
|
case "exec_command_end": {
|
|
2510
2546
|
const durationMs = durationObjectToMs(objectField(payload, "duration"));
|
|
2511
2547
|
const success = Number(payload.exit_code) === 0;
|
|
2512
|
-
|
|
2548
|
+
pushEvent(baseCodexEvent({
|
|
2513
2549
|
ts,
|
|
2514
2550
|
type: success ? "command.completed" : "command.failed",
|
|
2515
2551
|
operation: "command completed",
|
|
@@ -2530,7 +2566,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2530
2566
|
sourceId: stringField(payload, "call_id"),
|
|
2531
2567
|
commandHash: createStableHash(payload.command)
|
|
2532
2568
|
})
|
|
2533
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2569
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2534
2570
|
break;
|
|
2535
2571
|
}
|
|
2536
2572
|
case "patch_apply_end": {
|
|
@@ -2540,7 +2576,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2540
2576
|
cwd,
|
|
2541
2577
|
displayFilePath2
|
|
2542
2578
|
);
|
|
2543
|
-
|
|
2579
|
+
pushEvent(baseCodexEvent({
|
|
2544
2580
|
ts,
|
|
2545
2581
|
type: "file.changed",
|
|
2546
2582
|
operation: "apply patch",
|
|
@@ -2557,15 +2593,19 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2557
2593
|
refs: stringRefs({
|
|
2558
2594
|
sourceId: stringField(payload, "call_id")
|
|
2559
2595
|
})
|
|
2560
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2596
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2561
2597
|
break;
|
|
2562
2598
|
}
|
|
2563
2599
|
case "task_complete": {
|
|
2564
|
-
|
|
2600
|
+
const completedTurnId = stringField(payload, "turn_id") || currentTurnId;
|
|
2601
|
+
if (completedTurnId) {
|
|
2602
|
+
closedTurnIds.add(completedTurnId);
|
|
2603
|
+
}
|
|
2604
|
+
pushEvent(baseCodexEvent({
|
|
2565
2605
|
ts,
|
|
2566
2606
|
type: "turn.completed",
|
|
2567
2607
|
sessionId,
|
|
2568
|
-
turnId:
|
|
2608
|
+
turnId: completedTurnId,
|
|
2569
2609
|
cwd,
|
|
2570
2610
|
project,
|
|
2571
2611
|
model,
|
|
@@ -2573,15 +2613,15 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2573
2613
|
metrics: {
|
|
2574
2614
|
durationMs: numberField(payload, "duration_ms")
|
|
2575
2615
|
}
|
|
2576
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2616
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2577
2617
|
break;
|
|
2578
2618
|
}
|
|
2579
2619
|
}
|
|
2580
2620
|
}
|
|
2581
|
-
if (lastTurnIdForComplete) {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
ts:
|
|
2621
|
+
if (lastTurnIdForComplete && !closedTurnIds.has(lastTurnIdForComplete)) {
|
|
2622
|
+
closedTurnIds.add(lastTurnIdForComplete);
|
|
2623
|
+
pushEvent(baseCodexEvent({
|
|
2624
|
+
ts: turnCloseTs(lastTurnIdForComplete),
|
|
2585
2625
|
type: "turn.completed",
|
|
2586
2626
|
sessionId,
|
|
2587
2627
|
turnId: lastTurnIdForComplete,
|
|
@@ -2589,7 +2629,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2589
2629
|
project,
|
|
2590
2630
|
model,
|
|
2591
2631
|
confidence: "derived"
|
|
2592
|
-
}), { filePath, sourcePathHash, lineNumber: lines.length, topType: "event_msg", payloadType: "turn.completed", options })
|
|
2632
|
+
}), { filePath, sourcePathHash, lineNumber: lines.length, topType: "event_msg", payloadType: "turn.completed", options });
|
|
2593
2633
|
}
|
|
2594
2634
|
return events.filter((event) => validateCanonicalEvent(event).valid);
|
|
2595
2635
|
}
|
|
@@ -2628,10 +2668,11 @@ function headlessCodexUsage(raw) {
|
|
|
2628
2668
|
if (input === 0 && cached === 0 && output === 0 && reasoning === 0 && total === 0) {
|
|
2629
2669
|
return;
|
|
2630
2670
|
}
|
|
2671
|
+
const billableOutput = output + reasoning;
|
|
2631
2672
|
return {
|
|
2632
2673
|
tokensInput: input || void 0,
|
|
2633
2674
|
tokensCachedInput: cached || void 0,
|
|
2634
|
-
tokensOutput:
|
|
2675
|
+
tokensOutput: billableOutput || void 0,
|
|
2635
2676
|
tokensReasoningOutput: reasoning || void 0,
|
|
2636
2677
|
tokensTotal: total,
|
|
2637
2678
|
modelCalls: 1
|
|
@@ -2992,9 +3033,10 @@ async function parseGeminiSessionFile(filePath, options) {
|
|
|
2992
3033
|
let lastTs = usages[0].ts;
|
|
2993
3034
|
for (const [index, usage] of usages.entries()) {
|
|
2994
3035
|
lastTs = usage.ts;
|
|
3036
|
+
const billableOutput = usage.tokensOutput + usage.tokensReasoning;
|
|
2995
3037
|
const metrics = {
|
|
2996
3038
|
tokensInput: usage.tokensInput || void 0,
|
|
2997
|
-
tokensOutput:
|
|
3039
|
+
tokensOutput: billableOutput || void 0,
|
|
2998
3040
|
tokensCachedInput: usage.tokensCacheRead || void 0,
|
|
2999
3041
|
tokensCacheReadInput: usage.tokensCacheRead || void 0,
|
|
3000
3042
|
tokensReasoningOutput: usage.tokensReasoning || void 0,
|
|
@@ -3439,13 +3481,14 @@ function opencodeUsageFromInfo(info) {
|
|
|
3439
3481
|
const cacheRead = Math.max(0, numberField(cache, "read") || 0);
|
|
3440
3482
|
const cacheWrite = Math.max(0, numberField(cache, "write") || 0);
|
|
3441
3483
|
const totalInput = input + cacheRead + cacheWrite;
|
|
3442
|
-
const
|
|
3484
|
+
const billableOutput = output + reasoning;
|
|
3485
|
+
const total = Math.max(0, numberField(tokensObj, "total") || totalInput + billableOutput);
|
|
3443
3486
|
if (total <= 0) {
|
|
3444
3487
|
return void 0;
|
|
3445
3488
|
}
|
|
3446
3489
|
return {
|
|
3447
3490
|
tokensInput: totalInput || void 0,
|
|
3448
|
-
tokensOutput:
|
|
3491
|
+
tokensOutput: billableOutput || void 0,
|
|
3449
3492
|
tokensReasoningOutput: reasoning || void 0,
|
|
3450
3493
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
3451
3494
|
tokensCacheReadInput: cacheRead || void 0,
|
|
@@ -4110,9 +4153,11 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4110
4153
|
const lastEventAt = ordered.at(-1)?.ts || startedAt;
|
|
4111
4154
|
const timeBuckets = /* @__PURE__ */ new Map();
|
|
4112
4155
|
const modelRollups = /* @__PURE__ */ new Map();
|
|
4156
|
+
const modelBuckets = /* @__PURE__ */ new Map();
|
|
4113
4157
|
const toolRollups = /* @__PURE__ */ new Map();
|
|
4114
4158
|
const fileRollups = /* @__PURE__ */ new Map();
|
|
4115
4159
|
const turnRollups = /* @__PURE__ */ new Map();
|
|
4160
|
+
const turnEventTimes = /* @__PURE__ */ new Map();
|
|
4116
4161
|
let promptCount = 0;
|
|
4117
4162
|
let turnCount = 0;
|
|
4118
4163
|
let toolCallCount = 0;
|
|
@@ -4130,6 +4175,8 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4130
4175
|
const eventInputTokens = Math.max(0, event.metrics?.tokensInput || 0);
|
|
4131
4176
|
const eventCachedInputTokens = Math.max(0, event.metrics?.tokensCachedInput || 0);
|
|
4132
4177
|
const eventCacheCreationInputTokens = Math.max(0, event.metrics?.tokensCacheCreationInput || 0);
|
|
4178
|
+
const eventCacheCreation5mInputTokens = Math.max(0, event.metrics?.tokensCacheCreation5mInput || 0);
|
|
4179
|
+
const eventCacheCreation1hInputTokens = Math.max(0, event.metrics?.tokensCacheCreation1hInput || 0);
|
|
4133
4180
|
const eventCacheReadInputTokens = Math.max(0, event.metrics?.tokensCacheReadInput || 0);
|
|
4134
4181
|
const eventOutputTokens = Math.max(0, event.metrics?.tokensOutput || 0);
|
|
4135
4182
|
const eventReasoningOutputTokens = Math.max(0, event.metrics?.tokensReasoningOutput || 0);
|
|
@@ -4210,6 +4257,12 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4210
4257
|
turnRollup.outputTokens += eventOutputTokens;
|
|
4211
4258
|
turnRollup.totalTokens += eventTotalTokens;
|
|
4212
4259
|
turnRollups.set(event.turnId, turnRollup);
|
|
4260
|
+
const times = turnEventTimes.get(event.turnId);
|
|
4261
|
+
if (times) {
|
|
4262
|
+
times.push(event.ts);
|
|
4263
|
+
} else {
|
|
4264
|
+
turnEventTimes.set(event.turnId, [event.ts]);
|
|
4265
|
+
}
|
|
4213
4266
|
}
|
|
4214
4267
|
inputTokens += eventInputTokens;
|
|
4215
4268
|
cachedInputTokens += eventCachedInputTokens;
|
|
@@ -4244,6 +4297,8 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4244
4297
|
inputTokens: 0,
|
|
4245
4298
|
cachedInputTokens: 0,
|
|
4246
4299
|
cacheCreationInputTokens: 0,
|
|
4300
|
+
cacheCreation5mInputTokens: 0,
|
|
4301
|
+
cacheCreation1hInputTokens: 0,
|
|
4247
4302
|
cacheReadInputTokens: 0,
|
|
4248
4303
|
outputTokens: 0,
|
|
4249
4304
|
reasoningOutputTokens: 0,
|
|
@@ -4254,12 +4309,40 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4254
4309
|
modelRollup.inputTokens += eventInputTokens;
|
|
4255
4310
|
modelRollup.cachedInputTokens += eventCachedInputTokens;
|
|
4256
4311
|
modelRollup.cacheCreationInputTokens += eventCacheCreationInputTokens;
|
|
4312
|
+
modelRollup.cacheCreation5mInputTokens += eventCacheCreation5mInputTokens;
|
|
4313
|
+
modelRollup.cacheCreation1hInputTokens += eventCacheCreation1hInputTokens;
|
|
4257
4314
|
modelRollup.cacheReadInputTokens += eventCacheReadInputTokens;
|
|
4258
4315
|
modelRollup.outputTokens += eventOutputTokens;
|
|
4259
4316
|
modelRollup.reasoningOutputTokens += eventReasoningOutputTokens;
|
|
4260
4317
|
modelRollup.totalTokens += eventTotalTokens;
|
|
4261
4318
|
modelRollup.estimatedCostUsd += eventCostUsd;
|
|
4262
4319
|
modelRollups.set(modelKey, modelRollup);
|
|
4320
|
+
const modelBucketKey = `${bucketTs}\0${modelKey}`;
|
|
4321
|
+
const modelBucket = modelBuckets.get(modelBucketKey) || {
|
|
4322
|
+
ts: bucketTs,
|
|
4323
|
+
model: modelKey,
|
|
4324
|
+
callCount: 0,
|
|
4325
|
+
inputTokens: 0,
|
|
4326
|
+
cachedInputTokens: 0,
|
|
4327
|
+
cacheCreationInputTokens: 0,
|
|
4328
|
+
cacheCreation5mInputTokens: 0,
|
|
4329
|
+
cacheCreation1hInputTokens: 0,
|
|
4330
|
+
cacheReadInputTokens: 0,
|
|
4331
|
+
outputTokens: 0,
|
|
4332
|
+
reasoningOutputTokens: 0,
|
|
4333
|
+
totalTokens: 0
|
|
4334
|
+
};
|
|
4335
|
+
modelBucket.callCount += 1;
|
|
4336
|
+
modelBucket.inputTokens += eventInputTokens;
|
|
4337
|
+
modelBucket.cachedInputTokens += eventCachedInputTokens;
|
|
4338
|
+
modelBucket.cacheCreationInputTokens += eventCacheCreationInputTokens;
|
|
4339
|
+
modelBucket.cacheCreation5mInputTokens += eventCacheCreation5mInputTokens;
|
|
4340
|
+
modelBucket.cacheCreation1hInputTokens += eventCacheCreation1hInputTokens;
|
|
4341
|
+
modelBucket.cacheReadInputTokens += eventCacheReadInputTokens;
|
|
4342
|
+
modelBucket.outputTokens += eventOutputTokens;
|
|
4343
|
+
modelBucket.reasoningOutputTokens += eventReasoningOutputTokens;
|
|
4344
|
+
modelBucket.totalTokens += eventTotalTokens;
|
|
4345
|
+
modelBuckets.set(modelBucketKey, modelBucket);
|
|
4263
4346
|
}
|
|
4264
4347
|
if (event.type === "tool.started") {
|
|
4265
4348
|
bucket.toolCalls += 1;
|
|
@@ -4327,6 +4410,13 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4327
4410
|
const baseRollup = {
|
|
4328
4411
|
rollupKey,
|
|
4329
4412
|
payloadHash: "",
|
|
4413
|
+
// v3 schema: v2 (gap-clamped turn durations + billable-output token
|
|
4414
|
+
// convention) plus per-model cache-creation TTL split and modelBuckets. Set on
|
|
4415
|
+
// baseRollup (not after) so it participates in payloadHash: every historical
|
|
4416
|
+
// rollup's hash changes, and a re-backfill (uploaded with replace=true by
|
|
4417
|
+
// default) cleanly refreshes all data onto the new convention. This
|
|
4418
|
+
// full-refresh churn is intentional.
|
|
4419
|
+
schemaVersion: AGENT_ROLLUP_SCHEMA_VERSION,
|
|
4330
4420
|
source: first.source,
|
|
4331
4421
|
project,
|
|
4332
4422
|
sessionId,
|
|
@@ -4350,11 +4440,16 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4350
4440
|
durationMs: Math.max(0, Date.parse(lastEventAt) - Date.parse(startedAt)),
|
|
4351
4441
|
timeBuckets: [...timeBuckets.values()].sort((a, b) => a.ts.localeCompare(b.ts)),
|
|
4352
4442
|
modelRollups: [...modelRollups.values()].sort((a, b) => b.callCount - a.callCount || a.model.localeCompare(b.model)),
|
|
4443
|
+
// Sorted ts ascending, then model lexicographically (wire contract).
|
|
4444
|
+
modelBuckets: [...modelBuckets.values()].sort((a, b) => a.ts.localeCompare(b.ts) || a.model.localeCompare(b.model)),
|
|
4353
4445
|
toolRollups: [...toolRollups.values()].sort((a, b) => b.callCount - a.callCount || a.tool.localeCompare(b.tool)),
|
|
4354
4446
|
fileRollups: [...fileRollups.values()].sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.displayPath.localeCompare(b.displayPath)),
|
|
4355
4447
|
turnRollups: [...turnRollups.values()].map((rollup) => ({
|
|
4356
4448
|
...rollup,
|
|
4357
|
-
|
|
4449
|
+
// Gap-clamped active duration: startedAt/lastEventAt/completedAt keep their
|
|
4450
|
+
// real timestamps, but durationMs only counts active intervals so lazy
|
|
4451
|
+
// completed_at timestamps and long in-turn silences don't inflate it.
|
|
4452
|
+
durationMs: gapClampedTurnDurationMs(rollup, turnEventTimes.get(rollup.turnId) || [])
|
|
4358
4453
|
})).sort((a, b) => a.startedAt.localeCompare(b.startedAt))
|
|
4359
4454
|
};
|
|
4360
4455
|
return {
|
|
@@ -4362,6 +4457,22 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4362
4457
|
payloadHash: createPayloadHash(baseRollup)
|
|
4363
4458
|
};
|
|
4364
4459
|
}
|
|
4460
|
+
function gapClampedTurnDurationMs(rollup, eventTimes) {
|
|
4461
|
+
const millis = [
|
|
4462
|
+
rollup.promptSubmittedAt,
|
|
4463
|
+
rollup.startedAt,
|
|
4464
|
+
...eventTimes
|
|
4465
|
+
].map((ts) => ts ? Date.parse(ts) : Number.NaN).filter((ms) => Number.isFinite(ms));
|
|
4466
|
+
const unique = [...new Set(millis)].sort((a, b) => a - b);
|
|
4467
|
+
if (unique.length < 2) {
|
|
4468
|
+
return 0;
|
|
4469
|
+
}
|
|
4470
|
+
let duration = 0;
|
|
4471
|
+
for (let i = 1; i < unique.length; i += 1) {
|
|
4472
|
+
duration += Math.min(unique[i] - unique[i - 1], TURN_GAP_CLAMP_MS);
|
|
4473
|
+
}
|
|
4474
|
+
return Math.max(0, duration);
|
|
4475
|
+
}
|
|
4365
4476
|
function floorRollupBucket(ts) {
|
|
4366
4477
|
const ms = Date.parse(ts);
|
|
4367
4478
|
if (!Number.isFinite(ms)) {
|
|
@@ -4374,7 +4485,7 @@ function totalTokensFromEvent(event) {
|
|
|
4374
4485
|
if (typeof explicit === "number" && explicit > 0) {
|
|
4375
4486
|
return explicit;
|
|
4376
4487
|
}
|
|
4377
|
-
return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0)
|
|
4488
|
+
return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0);
|
|
4378
4489
|
}
|
|
4379
4490
|
function lineStatsFromEvent(event) {
|
|
4380
4491
|
const files = event.fileActivities || [];
|
package/package.json
CHANGED