codetime-cli 0.5.0 → 0.6.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 +100 -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 = 2;
|
|
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.6.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
|
|
@@ -2243,6 +2245,23 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2243
2245
|
let sessionMetaLocked = false;
|
|
2244
2246
|
let lastTokenUsageKey;
|
|
2245
2247
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2248
|
+
const turnLastEventAt = /* @__PURE__ */ new Map();
|
|
2249
|
+
const turnStartedAt = /* @__PURE__ */ new Map();
|
|
2250
|
+
const closedTurnIds = /* @__PURE__ */ new Set();
|
|
2251
|
+
const pushEvent = (event, refs) => {
|
|
2252
|
+
const turnId = event.turnId;
|
|
2253
|
+
if (turnId && event.ts) {
|
|
2254
|
+
if (!turnStartedAt.has(turnId)) {
|
|
2255
|
+
turnStartedAt.set(turnId, event.ts);
|
|
2256
|
+
}
|
|
2257
|
+
const prev = turnLastEventAt.get(turnId);
|
|
2258
|
+
if (!prev || event.ts > prev) {
|
|
2259
|
+
turnLastEventAt.set(turnId, event.ts);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
events.push(withBackfillRefs(event, refs));
|
|
2263
|
+
};
|
|
2264
|
+
const turnCloseTs = (turnId) => turnLastEventAt.get(turnId) || turnStartedAt.get(turnId) || (/* @__PURE__ */ new Date()).toISOString();
|
|
2246
2265
|
for (const [index, line] of lines.entries()) {
|
|
2247
2266
|
const lineNumber = index + 1;
|
|
2248
2267
|
const raw = parseJsonLine(line);
|
|
@@ -2299,23 +2318,23 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2299
2318
|
const eventModel = parsedModel || model || "gpt-5";
|
|
2300
2319
|
const headlessTs = headlessCodexTimestamp(raw) || ts;
|
|
2301
2320
|
if (!sessionStartEmitted) {
|
|
2302
|
-
|
|
2321
|
+
pushEvent(baseCodexEvent({
|
|
2303
2322
|
ts: headlessTs,
|
|
2304
2323
|
type: "session.started",
|
|
2305
2324
|
sessionId,
|
|
2306
2325
|
model: eventModel,
|
|
2307
2326
|
confidence: "derived"
|
|
2308
|
-
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "session", options })
|
|
2327
|
+
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "session", options });
|
|
2309
2328
|
sessionStartEmitted = true;
|
|
2310
2329
|
}
|
|
2311
|
-
|
|
2330
|
+
pushEvent(baseCodexEvent({
|
|
2312
2331
|
ts: headlessTs,
|
|
2313
2332
|
type: "model.usage",
|
|
2314
2333
|
sessionId,
|
|
2315
2334
|
model: eventModel,
|
|
2316
2335
|
confidence: "partial",
|
|
2317
2336
|
metrics: usage
|
|
2318
|
-
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "headless", options })
|
|
2337
|
+
}), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "headless", options });
|
|
2319
2338
|
}
|
|
2320
2339
|
continue;
|
|
2321
2340
|
}
|
|
@@ -2325,7 +2344,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2325
2344
|
switch (payloadType) {
|
|
2326
2345
|
case "task_started": {
|
|
2327
2346
|
currentTurnId = stringField(payload, "turn_id") || currentTurnId;
|
|
2328
|
-
|
|
2347
|
+
pushEvent(baseCodexEvent({
|
|
2329
2348
|
ts,
|
|
2330
2349
|
type: "turn.started",
|
|
2331
2350
|
sessionId,
|
|
@@ -2334,14 +2353,15 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2334
2353
|
project,
|
|
2335
2354
|
model,
|
|
2336
2355
|
confidence: "exact"
|
|
2337
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2356
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2338
2357
|
break;
|
|
2339
2358
|
}
|
|
2340
2359
|
case "user_message": {
|
|
2341
2360
|
const message = stringField(payload, "message") || "";
|
|
2342
|
-
if (currentTurnId && currentTurnId !== turnIdAtLastUserMessage && lastTurnIdForComplete) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2361
|
+
if (currentTurnId && currentTurnId !== turnIdAtLastUserMessage && lastTurnIdForComplete && !closedTurnIds.has(lastTurnIdForComplete)) {
|
|
2362
|
+
closedTurnIds.add(lastTurnIdForComplete);
|
|
2363
|
+
pushEvent(baseCodexEvent({
|
|
2364
|
+
ts: turnCloseTs(lastTurnIdForComplete),
|
|
2345
2365
|
type: "turn.completed",
|
|
2346
2366
|
sessionId,
|
|
2347
2367
|
turnId: lastTurnIdForComplete,
|
|
@@ -2349,14 +2369,14 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2349
2369
|
project,
|
|
2350
2370
|
model,
|
|
2351
2371
|
confidence: "derived"
|
|
2352
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2372
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2353
2373
|
}
|
|
2354
2374
|
if (!currentTurnId || currentTurnId === turnIdAtLastUserMessage) {
|
|
2355
2375
|
currentTurnId = `turn_${createStableHash([sessionId, lineNumber, ts]).slice(0, 24)}`;
|
|
2356
2376
|
}
|
|
2357
2377
|
lastTurnIdForComplete = currentTurnId;
|
|
2358
2378
|
turnIdAtLastUserMessage = currentTurnId;
|
|
2359
|
-
|
|
2379
|
+
pushEvent(baseCodexEvent({
|
|
2360
2380
|
ts,
|
|
2361
2381
|
type: "prompt.submitted",
|
|
2362
2382
|
sessionId,
|
|
@@ -2372,7 +2392,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2372
2392
|
refs: stringRefs({
|
|
2373
2393
|
promptHash: message ? `sha256:${createStableHash(message)}` : void 0
|
|
2374
2394
|
})
|
|
2375
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2395
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2376
2396
|
break;
|
|
2377
2397
|
}
|
|
2378
2398
|
case "token_count": {
|
|
@@ -2394,7 +2414,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2394
2414
|
break;
|
|
2395
2415
|
}
|
|
2396
2416
|
lastTokenUsageKey = usageKey;
|
|
2397
|
-
|
|
2417
|
+
pushEvent(baseCodexEvent({
|
|
2398
2418
|
ts,
|
|
2399
2419
|
type: "model.usage",
|
|
2400
2420
|
sessionId,
|
|
@@ -2407,13 +2427,13 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2407
2427
|
model: rewriteCodexModelForTier(model, serviceTier),
|
|
2408
2428
|
confidence: "partial",
|
|
2409
2429
|
metrics: usage
|
|
2410
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2430
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2411
2431
|
}
|
|
2412
2432
|
break;
|
|
2413
2433
|
}
|
|
2414
2434
|
case "agent_message": {
|
|
2415
2435
|
const message = stringField(payload, "message") || "";
|
|
2416
|
-
|
|
2436
|
+
pushEvent(baseCodexEvent({
|
|
2417
2437
|
ts,
|
|
2418
2438
|
type: "agent.operation",
|
|
2419
2439
|
operation: "agent message",
|
|
@@ -2426,7 +2446,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2426
2446
|
metrics: {
|
|
2427
2447
|
agentMessageChars: message.length
|
|
2428
2448
|
}
|
|
2429
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2449
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2430
2450
|
break;
|
|
2431
2451
|
}
|
|
2432
2452
|
case "function_call":
|
|
@@ -2438,7 +2458,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2438
2458
|
if (callId) {
|
|
2439
2459
|
pendingToolCalls.set(callId, { tool, startedAt: ts, turnId: currentTurnId });
|
|
2440
2460
|
}
|
|
2441
|
-
|
|
2461
|
+
pushEvent(baseCodexEvent({
|
|
2442
2462
|
ts,
|
|
2443
2463
|
type: "tool.started",
|
|
2444
2464
|
operation: `${tool} started`,
|
|
@@ -2455,9 +2475,9 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2455
2475
|
refs: stringRefs({
|
|
2456
2476
|
sourceId: callId
|
|
2457
2477
|
})
|
|
2458
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2478
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2459
2479
|
if (fileActivities.length > 0) {
|
|
2460
|
-
|
|
2480
|
+
pushEvent(baseCodexEvent({
|
|
2461
2481
|
ts,
|
|
2462
2482
|
type: eventTypeFromFileActivities(fileActivities),
|
|
2463
2483
|
operation: `${tool} file activity`,
|
|
@@ -2473,7 +2493,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2473
2493
|
refs: stringRefs({
|
|
2474
2494
|
sourceId: callId
|
|
2475
2495
|
})
|
|
2476
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2496
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2477
2497
|
}
|
|
2478
2498
|
break;
|
|
2479
2499
|
}
|
|
@@ -2485,7 +2505,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2485
2505
|
pendingToolCalls.delete(callId);
|
|
2486
2506
|
}
|
|
2487
2507
|
const durationMs = pending ? durationMsBetween(pending.startedAt, ts) : void 0;
|
|
2488
|
-
|
|
2508
|
+
pushEvent(baseCodexEvent({
|
|
2489
2509
|
ts,
|
|
2490
2510
|
type: "tool.completed",
|
|
2491
2511
|
operation: pending ? `${pending.tool} completed` : "tool completed",
|
|
@@ -2503,13 +2523,13 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2503
2523
|
refs: stringRefs({
|
|
2504
2524
|
sourceId: callId
|
|
2505
2525
|
})
|
|
2506
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2526
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2507
2527
|
break;
|
|
2508
2528
|
}
|
|
2509
2529
|
case "exec_command_end": {
|
|
2510
2530
|
const durationMs = durationObjectToMs(objectField(payload, "duration"));
|
|
2511
2531
|
const success = Number(payload.exit_code) === 0;
|
|
2512
|
-
|
|
2532
|
+
pushEvent(baseCodexEvent({
|
|
2513
2533
|
ts,
|
|
2514
2534
|
type: success ? "command.completed" : "command.failed",
|
|
2515
2535
|
operation: "command completed",
|
|
@@ -2530,7 +2550,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2530
2550
|
sourceId: stringField(payload, "call_id"),
|
|
2531
2551
|
commandHash: createStableHash(payload.command)
|
|
2532
2552
|
})
|
|
2533
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2553
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2534
2554
|
break;
|
|
2535
2555
|
}
|
|
2536
2556
|
case "patch_apply_end": {
|
|
@@ -2540,7 +2560,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2540
2560
|
cwd,
|
|
2541
2561
|
displayFilePath2
|
|
2542
2562
|
);
|
|
2543
|
-
|
|
2563
|
+
pushEvent(baseCodexEvent({
|
|
2544
2564
|
ts,
|
|
2545
2565
|
type: "file.changed",
|
|
2546
2566
|
operation: "apply patch",
|
|
@@ -2557,15 +2577,19 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2557
2577
|
refs: stringRefs({
|
|
2558
2578
|
sourceId: stringField(payload, "call_id")
|
|
2559
2579
|
})
|
|
2560
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2580
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2561
2581
|
break;
|
|
2562
2582
|
}
|
|
2563
2583
|
case "task_complete": {
|
|
2564
|
-
|
|
2584
|
+
const completedTurnId = stringField(payload, "turn_id") || currentTurnId;
|
|
2585
|
+
if (completedTurnId) {
|
|
2586
|
+
closedTurnIds.add(completedTurnId);
|
|
2587
|
+
}
|
|
2588
|
+
pushEvent(baseCodexEvent({
|
|
2565
2589
|
ts,
|
|
2566
2590
|
type: "turn.completed",
|
|
2567
2591
|
sessionId,
|
|
2568
|
-
turnId:
|
|
2592
|
+
turnId: completedTurnId,
|
|
2569
2593
|
cwd,
|
|
2570
2594
|
project,
|
|
2571
2595
|
model,
|
|
@@ -2573,15 +2597,15 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2573
2597
|
metrics: {
|
|
2574
2598
|
durationMs: numberField(payload, "duration_ms")
|
|
2575
2599
|
}
|
|
2576
|
-
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options })
|
|
2600
|
+
}), { filePath, sourcePathHash, lineNumber, topType, payloadType, options });
|
|
2577
2601
|
break;
|
|
2578
2602
|
}
|
|
2579
2603
|
}
|
|
2580
2604
|
}
|
|
2581
|
-
if (lastTurnIdForComplete) {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
ts:
|
|
2605
|
+
if (lastTurnIdForComplete && !closedTurnIds.has(lastTurnIdForComplete)) {
|
|
2606
|
+
closedTurnIds.add(lastTurnIdForComplete);
|
|
2607
|
+
pushEvent(baseCodexEvent({
|
|
2608
|
+
ts: turnCloseTs(lastTurnIdForComplete),
|
|
2585
2609
|
type: "turn.completed",
|
|
2586
2610
|
sessionId,
|
|
2587
2611
|
turnId: lastTurnIdForComplete,
|
|
@@ -2589,7 +2613,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2589
2613
|
project,
|
|
2590
2614
|
model,
|
|
2591
2615
|
confidence: "derived"
|
|
2592
|
-
}), { filePath, sourcePathHash, lineNumber: lines.length, topType: "event_msg", payloadType: "turn.completed", options })
|
|
2616
|
+
}), { filePath, sourcePathHash, lineNumber: lines.length, topType: "event_msg", payloadType: "turn.completed", options });
|
|
2593
2617
|
}
|
|
2594
2618
|
return events.filter((event) => validateCanonicalEvent(event).valid);
|
|
2595
2619
|
}
|
|
@@ -2628,10 +2652,11 @@ function headlessCodexUsage(raw) {
|
|
|
2628
2652
|
if (input === 0 && cached === 0 && output === 0 && reasoning === 0 && total === 0) {
|
|
2629
2653
|
return;
|
|
2630
2654
|
}
|
|
2655
|
+
const billableOutput = output + reasoning;
|
|
2631
2656
|
return {
|
|
2632
2657
|
tokensInput: input || void 0,
|
|
2633
2658
|
tokensCachedInput: cached || void 0,
|
|
2634
|
-
tokensOutput:
|
|
2659
|
+
tokensOutput: billableOutput || void 0,
|
|
2635
2660
|
tokensReasoningOutput: reasoning || void 0,
|
|
2636
2661
|
tokensTotal: total,
|
|
2637
2662
|
modelCalls: 1
|
|
@@ -2992,9 +3017,10 @@ async function parseGeminiSessionFile(filePath, options) {
|
|
|
2992
3017
|
let lastTs = usages[0].ts;
|
|
2993
3018
|
for (const [index, usage] of usages.entries()) {
|
|
2994
3019
|
lastTs = usage.ts;
|
|
3020
|
+
const billableOutput = usage.tokensOutput + usage.tokensReasoning;
|
|
2995
3021
|
const metrics = {
|
|
2996
3022
|
tokensInput: usage.tokensInput || void 0,
|
|
2997
|
-
tokensOutput:
|
|
3023
|
+
tokensOutput: billableOutput || void 0,
|
|
2998
3024
|
tokensCachedInput: usage.tokensCacheRead || void 0,
|
|
2999
3025
|
tokensCacheReadInput: usage.tokensCacheRead || void 0,
|
|
3000
3026
|
tokensReasoningOutput: usage.tokensReasoning || void 0,
|
|
@@ -3439,13 +3465,14 @@ function opencodeUsageFromInfo(info) {
|
|
|
3439
3465
|
const cacheRead = Math.max(0, numberField(cache, "read") || 0);
|
|
3440
3466
|
const cacheWrite = Math.max(0, numberField(cache, "write") || 0);
|
|
3441
3467
|
const totalInput = input + cacheRead + cacheWrite;
|
|
3442
|
-
const
|
|
3468
|
+
const billableOutput = output + reasoning;
|
|
3469
|
+
const total = Math.max(0, numberField(tokensObj, "total") || totalInput + billableOutput);
|
|
3443
3470
|
if (total <= 0) {
|
|
3444
3471
|
return void 0;
|
|
3445
3472
|
}
|
|
3446
3473
|
return {
|
|
3447
3474
|
tokensInput: totalInput || void 0,
|
|
3448
|
-
tokensOutput:
|
|
3475
|
+
tokensOutput: billableOutput || void 0,
|
|
3449
3476
|
tokensReasoningOutput: reasoning || void 0,
|
|
3450
3477
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
3451
3478
|
tokensCacheReadInput: cacheRead || void 0,
|
|
@@ -4113,6 +4140,7 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4113
4140
|
const toolRollups = /* @__PURE__ */ new Map();
|
|
4114
4141
|
const fileRollups = /* @__PURE__ */ new Map();
|
|
4115
4142
|
const turnRollups = /* @__PURE__ */ new Map();
|
|
4143
|
+
const turnEventTimes = /* @__PURE__ */ new Map();
|
|
4116
4144
|
let promptCount = 0;
|
|
4117
4145
|
let turnCount = 0;
|
|
4118
4146
|
let toolCallCount = 0;
|
|
@@ -4210,6 +4238,12 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4210
4238
|
turnRollup.outputTokens += eventOutputTokens;
|
|
4211
4239
|
turnRollup.totalTokens += eventTotalTokens;
|
|
4212
4240
|
turnRollups.set(event.turnId, turnRollup);
|
|
4241
|
+
const times = turnEventTimes.get(event.turnId);
|
|
4242
|
+
if (times) {
|
|
4243
|
+
times.push(event.ts);
|
|
4244
|
+
} else {
|
|
4245
|
+
turnEventTimes.set(event.turnId, [event.ts]);
|
|
4246
|
+
}
|
|
4213
4247
|
}
|
|
4214
4248
|
inputTokens += eventInputTokens;
|
|
4215
4249
|
cachedInputTokens += eventCachedInputTokens;
|
|
@@ -4327,6 +4361,12 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4327
4361
|
const baseRollup = {
|
|
4328
4362
|
rollupKey,
|
|
4329
4363
|
payloadHash: "",
|
|
4364
|
+
// v2 schema: trustworthy gap-clamped turn durations + billable-output token
|
|
4365
|
+
// convention. Set on baseRollup (not after) so it participates in payloadHash:
|
|
4366
|
+
// every historical rollup's hash changes, and a re-backfill (uploaded with
|
|
4367
|
+
// replace=true by default) cleanly refreshes all data onto the new convention.
|
|
4368
|
+
// This full-refresh churn is intentional.
|
|
4369
|
+
schemaVersion: AGENT_ROLLUP_SCHEMA_VERSION,
|
|
4330
4370
|
source: first.source,
|
|
4331
4371
|
project,
|
|
4332
4372
|
sessionId,
|
|
@@ -4354,7 +4394,10 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4354
4394
|
fileRollups: [...fileRollups.values()].sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.displayPath.localeCompare(b.displayPath)),
|
|
4355
4395
|
turnRollups: [...turnRollups.values()].map((rollup) => ({
|
|
4356
4396
|
...rollup,
|
|
4357
|
-
|
|
4397
|
+
// Gap-clamped active duration: startedAt/lastEventAt/completedAt keep their
|
|
4398
|
+
// real timestamps, but durationMs only counts active intervals so lazy
|
|
4399
|
+
// completed_at timestamps and long in-turn silences don't inflate it.
|
|
4400
|
+
durationMs: gapClampedTurnDurationMs(rollup, turnEventTimes.get(rollup.turnId) || [])
|
|
4358
4401
|
})).sort((a, b) => a.startedAt.localeCompare(b.startedAt))
|
|
4359
4402
|
};
|
|
4360
4403
|
return {
|
|
@@ -4362,6 +4405,22 @@ function buildSessionRollup(rollupKey, events) {
|
|
|
4362
4405
|
payloadHash: createPayloadHash(baseRollup)
|
|
4363
4406
|
};
|
|
4364
4407
|
}
|
|
4408
|
+
function gapClampedTurnDurationMs(rollup, eventTimes) {
|
|
4409
|
+
const millis = [
|
|
4410
|
+
rollup.promptSubmittedAt,
|
|
4411
|
+
rollup.startedAt,
|
|
4412
|
+
...eventTimes
|
|
4413
|
+
].map((ts) => ts ? Date.parse(ts) : Number.NaN).filter((ms) => Number.isFinite(ms));
|
|
4414
|
+
const unique = [...new Set(millis)].sort((a, b) => a - b);
|
|
4415
|
+
if (unique.length < 2) {
|
|
4416
|
+
return 0;
|
|
4417
|
+
}
|
|
4418
|
+
let duration = 0;
|
|
4419
|
+
for (let i = 1; i < unique.length; i += 1) {
|
|
4420
|
+
duration += Math.min(unique[i] - unique[i - 1], TURN_GAP_CLAMP_MS);
|
|
4421
|
+
}
|
|
4422
|
+
return Math.max(0, duration);
|
|
4423
|
+
}
|
|
4365
4424
|
function floorRollupBucket(ts) {
|
|
4366
4425
|
const ms = Date.parse(ts);
|
|
4367
4426
|
if (!Number.isFinite(ms)) {
|
|
@@ -4374,7 +4433,7 @@ function totalTokensFromEvent(event) {
|
|
|
4374
4433
|
if (typeof explicit === "number" && explicit > 0) {
|
|
4375
4434
|
return explicit;
|
|
4376
4435
|
}
|
|
4377
|
-
return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0)
|
|
4436
|
+
return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0);
|
|
4378
4437
|
}
|
|
4379
4438
|
function lineStatsFromEvent(event) {
|
|
4380
4439
|
const files = event.fileActivities || [];
|
package/package.json
CHANGED