codexmeter 1.0.8 → 1.0.9
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/dist/assets/index-DQDmrG5E.js +34 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/aggregator.js +37 -19
- package/server/ingest.js +210 -27
- package/server/live-state.js +52 -3
- package/server/normalize.js +64 -5
- package/server/pricing-fetch.js +2 -0
- package/server/rollout-reader.js +138 -9
- package/server/sqlite-reader.js +1 -1
- package/dist/assets/index-B1DauiX5.js +0 -34
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DQDmrG5E.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-CVvcsIkn.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/package.json
CHANGED
package/server/aggregator.js
CHANGED
|
@@ -262,11 +262,10 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
for (const s of
|
|
265
|
+
for (const s of rawSessions) {
|
|
266
266
|
if (!s.active_by_day) continue;
|
|
267
267
|
for (const [dayKey, seconds] of Object.entries(s.active_by_day)) {
|
|
268
|
-
|
|
269
|
-
dayMap.get(dayKey).elapsed_seconds += seconds;
|
|
268
|
+
addElapsedToDay(dayMap, dayKey, s, seconds || 0);
|
|
270
269
|
}
|
|
271
270
|
}
|
|
272
271
|
|
|
@@ -282,10 +281,10 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
282
281
|
Object.entries(d.by_model).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, elapsed_seconds: Math.round(v.elapsed_seconds) }])
|
|
283
282
|
),
|
|
284
283
|
by_family: Object.fromEntries(
|
|
285
|
-
Object.entries(d.by_family).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, sessions: v.sessions }])
|
|
284
|
+
Object.entries(d.by_family).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, elapsed_seconds: Math.round(v.elapsed_seconds), sessions: v.sessions }])
|
|
286
285
|
),
|
|
287
286
|
by_repo: Object.fromEntries(
|
|
288
|
-
Object.entries(d.by_repo || {}).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, sessions: v.sessions }])
|
|
287
|
+
Object.entries(d.by_repo || {}).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, elapsed_seconds: Math.round(v.elapsed_seconds), sessions: v.sessions }])
|
|
289
288
|
),
|
|
290
289
|
approximate: true,
|
|
291
290
|
}));
|
|
@@ -304,13 +303,13 @@ function addToDay(dayMap, dayKey, session, fraction) {
|
|
|
304
303
|
if (session.cost !== null) d.by_model[mKey].cost += session.cost * fraction;
|
|
305
304
|
|
|
306
305
|
const fKey = session.agent_family;
|
|
307
|
-
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
306
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
308
307
|
d.by_family[fKey].tokens += session.tokens_used * fraction;
|
|
309
308
|
if (session.cost !== null) d.by_family[fKey].cost += session.cost * fraction;
|
|
310
309
|
if (fraction > 0.001) d.by_family[fKey].sessions++;
|
|
311
310
|
|
|
312
311
|
const rKey = session.repo_label || 'unknown';
|
|
313
|
-
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
312
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
314
313
|
d.by_repo[rKey].tokens += session.tokens_used * fraction;
|
|
315
314
|
if (session.cost !== null) d.by_repo[rKey].cost += session.cost * fraction;
|
|
316
315
|
if (fraction > 0.001) d.by_repo[rKey].sessions++;
|
|
@@ -375,11 +374,11 @@ function addPresenceToDay(dayMap, dayKey, session, fraction) {
|
|
|
375
374
|
if (fraction > 0.001) d.sessions++;
|
|
376
375
|
|
|
377
376
|
const fKey = session.agent_family;
|
|
378
|
-
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
377
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
379
378
|
if (fraction > 0.001) d.by_family[fKey].sessions++;
|
|
380
379
|
|
|
381
380
|
const rKey = session.repo_label || 'unknown';
|
|
382
|
-
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
381
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
383
382
|
if (fraction > 0.001) d.by_repo[rKey].sessions++;
|
|
384
383
|
}
|
|
385
384
|
|
|
@@ -397,17 +396,36 @@ function addUsageByDay(dayMap, session) {
|
|
|
397
396
|
if (usageDay.cost !== null) d.by_model[mKey].cost += usageDay.cost;
|
|
398
397
|
|
|
399
398
|
const fKey = session.agent_family;
|
|
400
|
-
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
399
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
401
400
|
d.by_family[fKey].tokens += usageDay.tokens || 0;
|
|
402
401
|
if (usageDay.cost !== null) d.by_family[fKey].cost += usageDay.cost;
|
|
403
402
|
|
|
404
403
|
const rKey = session.repo_label || 'unknown';
|
|
405
|
-
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
404
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
406
405
|
d.by_repo[rKey].tokens += usageDay.tokens || 0;
|
|
407
406
|
if (usageDay.cost !== null) d.by_repo[rKey].cost += usageDay.cost;
|
|
408
407
|
}
|
|
409
408
|
}
|
|
410
409
|
|
|
410
|
+
function addElapsedToDay(dayMap, dayKey, session, seconds) {
|
|
411
|
+
if (!seconds) return;
|
|
412
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
413
|
+
const d = dayMap.get(dayKey);
|
|
414
|
+
d.elapsed_seconds += seconds;
|
|
415
|
+
|
|
416
|
+
const mKey = session.model_name || 'unknown';
|
|
417
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
418
|
+
d.by_model[mKey].elapsed_seconds += seconds;
|
|
419
|
+
|
|
420
|
+
const fKey = session.agent_family || 'generic';
|
|
421
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
422
|
+
d.by_family[fKey].elapsed_seconds += seconds;
|
|
423
|
+
|
|
424
|
+
const rKey = session.repo_label || 'unknown';
|
|
425
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
426
|
+
d.by_repo[rKey].elapsed_seconds += seconds;
|
|
427
|
+
}
|
|
428
|
+
|
|
411
429
|
function dayStartMs(dayKey) {
|
|
412
430
|
const [y, m, d] = dayKey.split('-').map(Number);
|
|
413
431
|
return new Date(y, m - 1, d).getTime();
|
|
@@ -462,11 +480,11 @@ function collapseSessionGroup(rootThreadId, group, rootLookup) {
|
|
|
462
480
|
thread_id: root.thread_id,
|
|
463
481
|
root_thread_id: rootThreadId,
|
|
464
482
|
repo_label: repoLabels.size === 1 ? [...repoLabels][0] : (root.repo_label || 'mixed'),
|
|
465
|
-
model_name: root.model_name
|
|
466
|
-
reasoning_effort: root.reasoning_effort
|
|
467
|
-
agent_role: root.agent_role,
|
|
468
|
-
agent_nickname: root.agent_nickname,
|
|
469
|
-
agent_family: root.agent_family,
|
|
483
|
+
model_name: summarizeGroupedValue(root.model_name, modelNames),
|
|
484
|
+
reasoning_effort: summarizeGroupedValue(root.reasoning_effort, efforts),
|
|
485
|
+
agent_role: summarizeGroupedValue(root.agent_role, agentRoles),
|
|
486
|
+
agent_nickname: summarizeGroupedValue(root.agent_nickname, agentNicknames),
|
|
487
|
+
agent_family: summarizeGroupedValue(root.agent_family, agentFamilySet),
|
|
470
488
|
is_subagent: false,
|
|
471
489
|
started_at: rootStartedAt,
|
|
472
490
|
ended_at: rootEndedAt,
|
|
@@ -490,10 +508,10 @@ function collapseSessionGroup(rootThreadId, group, rootLookup) {
|
|
|
490
508
|
};
|
|
491
509
|
}
|
|
492
510
|
|
|
493
|
-
function
|
|
494
|
-
if (values.size === 0) return null;
|
|
511
|
+
function summarizeGroupedValue(rootValue, values) {
|
|
512
|
+
if (values.size === 0) return rootValue || null;
|
|
495
513
|
if (values.size === 1) return [...values][0];
|
|
496
|
-
return
|
|
514
|
+
return 'mixed';
|
|
497
515
|
}
|
|
498
516
|
|
|
499
517
|
function mergeActiveByDay(target, source) {
|
package/server/ingest.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readThreads } from './sqlite-reader.js';
|
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import {
|
|
4
4
|
normalizeCwd, deriveRepoKey, deriveRepoLabel,
|
|
5
|
-
classifyAgentFamily, isSubagent, normalizeModelName,
|
|
5
|
+
classifyAgentFamily, deriveAgentRole, isReviewLauncherSession, isSubagent, normalizeModelName,
|
|
6
6
|
} from './normalize.js';
|
|
7
7
|
import { calculateCostFromUsage, initPricing, priceSession } from './cost-catalog.js';
|
|
8
8
|
import { buildAggregates, buildSessionView } from './aggregator.js';
|
|
@@ -10,6 +10,7 @@ import { createDayKeyFormatter } from './day-key.js';
|
|
|
10
10
|
import { createLiveAggregateState, createEmptyLivePatch, applySessionToLiveState, buildLiveBootstrap, buildLivePatch } from './live-state.js';
|
|
11
11
|
import { createRolloutWorkerPool } from './rollout-worker-pool.js';
|
|
12
12
|
import { beginReplayCapture, createReplayCaptureState, failReplayCapture, getReplaySnapshot, recordReplayEvent, resetReplayCapture } from './export-replay.js';
|
|
13
|
+
import { findUsageEntryAtOrBefore, hasUsageTotals, readUsageTimeline, subtractUsageTotals } from './rollout-reader.js';
|
|
13
14
|
import { OVERVIEW_INGEST_ANIMATION } from '../src/utils/animationsDefault.js';
|
|
14
15
|
|
|
15
16
|
const LIVE_FRAME_INTERVAL_MS = Math.max(
|
|
@@ -99,9 +100,11 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
99
100
|
const sessions = [];
|
|
100
101
|
for (const t of threads) {
|
|
101
102
|
const nc = normalizeCwd(t.cwd_raw);
|
|
103
|
+
const agentRole = deriveAgentRole(t.agent_role, t.source_raw, t.title);
|
|
102
104
|
sessions.push({
|
|
103
105
|
thread_id: t.thread_id,
|
|
104
106
|
rollout_path: t.rollout_path,
|
|
107
|
+
source_raw: t.source_raw,
|
|
105
108
|
cwd_raw: t.cwd_raw,
|
|
106
109
|
repo_key: deriveRepoKey(nc),
|
|
107
110
|
repo_label: deriveRepoLabel(nc),
|
|
@@ -117,11 +120,12 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
117
120
|
has_usage_by_day: false,
|
|
118
121
|
live_sort_ts: null,
|
|
119
122
|
active_by_day: null,
|
|
120
|
-
agent_role:
|
|
123
|
+
agent_role: agentRole,
|
|
121
124
|
agent_nickname: t.agent_nickname,
|
|
122
|
-
agent_family: classifyAgentFamily(
|
|
123
|
-
is_subagent: isSubagent(
|
|
125
|
+
agent_family: classifyAgentFamily(agentRole),
|
|
126
|
+
is_subagent: isSubagent(agentRole),
|
|
124
127
|
parent_thread_id: null,
|
|
128
|
+
forked_from_id: null,
|
|
125
129
|
cost: null,
|
|
126
130
|
cost_source: 'unavailable',
|
|
127
131
|
materialized: !t.rollout_path,
|
|
@@ -133,9 +137,10 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
133
137
|
state.phase = 'enrichment';
|
|
134
138
|
queueLiveProgress(state);
|
|
135
139
|
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
140
|
+
const bootstrapCandidates = sessions.filter((session) => !session.rollout_path);
|
|
141
|
+
if (bootstrapCandidates.length) {
|
|
138
142
|
assignRootThreadIds(sessions);
|
|
143
|
+
const bootstrapSessions = filterVisibleSessions(bootstrapCandidates);
|
|
139
144
|
const bootstrapPatch = createEmptyLivePatch();
|
|
140
145
|
for (const session of bootstrapSessions) {
|
|
141
146
|
finalizeSessionMetrics(session, toDayKey);
|
|
@@ -144,14 +149,13 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
144
149
|
queueLivePatch(state, bootstrapPatch);
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
const candidates = sessions
|
|
148
|
-
.filter(s => s.rollout_path)
|
|
149
|
-
.sort((a, b) => (a.started_at || 0) - (b.started_at || 0));
|
|
152
|
+
const candidates = selectEnrichmentCandidates(sessions);
|
|
150
153
|
|
|
151
154
|
state.needs_enrichment = candidates.length;
|
|
152
155
|
state.percent = candidates.length > 0 ? 0.08 : 0.90;
|
|
153
156
|
const BATCH_SIZE = opts.batchSize || 40;
|
|
154
157
|
const ROOT_REFRESH_EVERY = opts.rootRefreshEvery || 1000;
|
|
158
|
+
const resolveForkUsageSnapshot = createForkUsageSnapshotResolver(sessions);
|
|
155
159
|
let lastRootRefreshCount = 0;
|
|
156
160
|
|
|
157
161
|
for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
|
|
@@ -177,12 +181,18 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
177
181
|
if (data.model_name) s.model_name = normalizeModelName(data.model_name);
|
|
178
182
|
if (data.reasoning_effort) s.reasoning_effort = data.reasoning_effort;
|
|
179
183
|
if (data.parent_thread_id) s.parent_thread_id = data.parent_thread_id;
|
|
184
|
+
if (data.forked_from_id) s.forked_from_id = data.forked_from_id;
|
|
180
185
|
if (data.usage_total) s.usage_total = data.usage_total;
|
|
186
|
+
if (data.usage_by_day) s._usage_by_day_raw = structuredClone(data.usage_by_day);
|
|
181
187
|
if (data.usage_by_day) {
|
|
182
188
|
s.usage_by_day = buildUsageByDayMetrics(s.model_name, data.usage_by_day);
|
|
183
189
|
s.has_usage_by_day = s.usage_by_day.length > 0;
|
|
184
190
|
}
|
|
191
|
+
if (data.usage_reset_detected) {
|
|
192
|
+
s._usage_reset_detected = true;
|
|
193
|
+
}
|
|
185
194
|
if (data.first_usage_timestamp) {
|
|
195
|
+
s._first_usage_timestamp_ms = data.first_usage_timestamp;
|
|
186
196
|
s.live_sort_ts = Math.floor(data.first_usage_timestamp / 1000);
|
|
187
197
|
}
|
|
188
198
|
if (data.active_seconds && data.active_seconds > 0) {
|
|
@@ -190,6 +200,17 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
190
200
|
s.active_by_day = data.active_by_day || null;
|
|
191
201
|
}
|
|
192
202
|
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await applyForkUsageCorrections(batch, resolveForkUsageSnapshot);
|
|
206
|
+
for (const s of batch) {
|
|
207
|
+
if (s._usage_by_day_raw) {
|
|
208
|
+
s.usage_by_day = buildUsageByDayMetrics(s.model_name, s._usage_by_day_raw);
|
|
209
|
+
s.has_usage_by_day = s.usage_by_day.length > 0;
|
|
210
|
+
}
|
|
211
|
+
delete s._usage_by_day_raw;
|
|
212
|
+
delete s._first_usage_timestamp_ms;
|
|
213
|
+
delete s._usage_reset_detected;
|
|
193
214
|
finalizeSessionMetrics(s, toDayKey);
|
|
194
215
|
s.live_sort_day = deriveLiveSortDay(s, toDayKey);
|
|
195
216
|
s.materialized = true;
|
|
@@ -204,13 +225,12 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
204
225
|
lastRootRefreshCount = state.enriched;
|
|
205
226
|
}
|
|
206
227
|
const liveReady = bufferLiveSessionsForPresentation(state, batch);
|
|
207
|
-
|
|
208
|
-
state.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
228
|
+
state.current_date_bucket = pickVisibleDateBucket(
|
|
229
|
+
filterVisibleSessions(state.live_session_buffer),
|
|
230
|
+
filterVisibleSessions(liveReady)
|
|
231
|
+
);
|
|
212
232
|
const livePatch = createEmptyLivePatch();
|
|
213
|
-
for (const session of liveReady) {
|
|
233
|
+
for (const session of filterVisibleSessions(liveReady)) {
|
|
214
234
|
applySessionToLiveState(state.live_state, session, livePatch);
|
|
215
235
|
}
|
|
216
236
|
queueLivePatch(state, livePatch);
|
|
@@ -230,7 +250,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
230
250
|
|
|
231
251
|
assignRootThreadIds(sessions);
|
|
232
252
|
const finalLivePatch = createEmptyLivePatch();
|
|
233
|
-
for (const session of flushBufferedLiveSessions(state)) {
|
|
253
|
+
for (const session of filterVisibleSessions(flushBufferedLiveSessions(state))) {
|
|
234
254
|
applySessionToLiveState(state.live_state, session, finalLivePatch);
|
|
235
255
|
}
|
|
236
256
|
queueLivePatch(state, finalLivePatch);
|
|
@@ -296,13 +316,28 @@ export function restartIngest(codexHome, state, opts = {}) {
|
|
|
296
316
|
|
|
297
317
|
function rebuildAggregates(sessions, state, opts, tz, mode = {}) {
|
|
298
318
|
const source = mode.partial ? sessions.filter(session => session.materialized) : sessions;
|
|
299
|
-
const
|
|
319
|
+
const visibleSource = filterVisibleSessions(source);
|
|
320
|
+
const filtered = applyFilters(visibleSource, opts);
|
|
300
321
|
const sessionView = buildSessionView(filtered, source);
|
|
301
322
|
state.sessions = sessionView;
|
|
302
323
|
state.aggregates = buildAggregates(filtered, tz, sessionView);
|
|
303
324
|
state.generated_at = new Date().toISOString();
|
|
304
325
|
}
|
|
305
326
|
|
|
327
|
+
export function filterVisibleSessions(sessions) {
|
|
328
|
+
return sessions.filter((session) => !isReviewLauncherSession(session));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function selectEnrichmentCandidates(sessions) {
|
|
332
|
+
return sessions
|
|
333
|
+
.filter((session) => session.rollout_path)
|
|
334
|
+
.sort((a, b) => (a.started_at || 0) - (b.started_at || 0));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function pickVisibleDateBucket(bufferedSessions, liveReadySessions) {
|
|
338
|
+
return bufferedSessions[0]?.live_sort_day || liveReadySessions[0]?.live_sort_day || null;
|
|
339
|
+
}
|
|
340
|
+
|
|
306
341
|
function applyFilters(sessions, opts) {
|
|
307
342
|
let result = sessions;
|
|
308
343
|
if (opts.from) {
|
|
@@ -325,7 +360,7 @@ function applyFilters(sessions, opts) {
|
|
|
325
360
|
return result;
|
|
326
361
|
}
|
|
327
362
|
|
|
328
|
-
function assignRootThreadIds(sessions) {
|
|
363
|
+
export function assignRootThreadIds(sessions, toDayKey = createDayKeyFormatter(Intl.DateTimeFormat().resolvedOptions().timeZone)) {
|
|
329
364
|
const byId = new Map(sessions.map(session => [session.thread_id, session]));
|
|
330
365
|
const memo = new Map();
|
|
331
366
|
|
|
@@ -336,37 +371,63 @@ function assignRootThreadIds(sessions) {
|
|
|
336
371
|
const trail = [];
|
|
337
372
|
const seen = new Set();
|
|
338
373
|
let current = session;
|
|
339
|
-
let
|
|
374
|
+
let root = {
|
|
375
|
+
id: session.thread_id,
|
|
376
|
+
dayKey: session.started_at ? toDayKey(session.started_at * 1000) : null,
|
|
377
|
+
};
|
|
340
378
|
|
|
341
379
|
while (current) {
|
|
342
380
|
trail.push(current.thread_id);
|
|
343
381
|
const parentId = current.parent_thread_id;
|
|
344
382
|
|
|
345
383
|
if (!parentId || parentId === current.thread_id || seen.has(parentId)) {
|
|
346
|
-
|
|
384
|
+
root = {
|
|
385
|
+
id: current.thread_id,
|
|
386
|
+
dayKey: current.started_at ? toDayKey(current.started_at * 1000) : null,
|
|
387
|
+
};
|
|
347
388
|
break;
|
|
348
389
|
}
|
|
349
390
|
|
|
350
391
|
if (memo.has(parentId)) {
|
|
351
|
-
|
|
392
|
+
const parentRoot = memo.get(parentId);
|
|
393
|
+
const currentDayKey = current.started_at ? toDayKey(current.started_at * 1000) : null;
|
|
394
|
+
root = parentRoot?.dayKey && currentDayKey === parentRoot.dayKey
|
|
395
|
+
? parentRoot
|
|
396
|
+
: {
|
|
397
|
+
id: current.thread_id,
|
|
398
|
+
dayKey: currentDayKey,
|
|
399
|
+
};
|
|
352
400
|
break;
|
|
353
401
|
}
|
|
354
402
|
|
|
355
403
|
seen.add(parentId);
|
|
356
404
|
const parent = byId.get(parentId);
|
|
357
405
|
if (!parent) {
|
|
358
|
-
|
|
406
|
+
root = {
|
|
407
|
+
id: parentId,
|
|
408
|
+
dayKey: current.started_at ? toDayKey(current.started_at * 1000) : null,
|
|
409
|
+
};
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const parentDayKey = parent.started_at ? toDayKey(parent.started_at * 1000) : null;
|
|
414
|
+
const currentDayKey = current.started_at ? toDayKey(current.started_at * 1000) : null;
|
|
415
|
+
if (currentDayKey && parentDayKey && currentDayKey !== parentDayKey) {
|
|
416
|
+
root = {
|
|
417
|
+
id: current.thread_id,
|
|
418
|
+
dayKey: currentDayKey,
|
|
419
|
+
};
|
|
359
420
|
break;
|
|
360
421
|
}
|
|
361
422
|
current = parent;
|
|
362
423
|
}
|
|
363
424
|
|
|
364
|
-
for (const threadId of trail) memo.set(threadId,
|
|
365
|
-
return
|
|
425
|
+
for (const threadId of trail) memo.set(threadId, root);
|
|
426
|
+
return root;
|
|
366
427
|
};
|
|
367
428
|
|
|
368
429
|
for (const session of sessions) {
|
|
369
|
-
session.root_thread_id = resolveRoot(session) || session.thread_id;
|
|
430
|
+
session.root_thread_id = resolveRoot(session)?.id || session.thread_id;
|
|
370
431
|
}
|
|
371
432
|
}
|
|
372
433
|
|
|
@@ -407,6 +468,130 @@ function buildUsageByDayMetrics(modelName, usageByDay) {
|
|
|
407
468
|
return entries;
|
|
408
469
|
}
|
|
409
470
|
|
|
471
|
+
function createForkUsageSnapshotResolver(sessions) {
|
|
472
|
+
const byId = new Map(sessions.map((session) => [session.thread_id, session]));
|
|
473
|
+
const timelineCache = new Map();
|
|
474
|
+
|
|
475
|
+
return async function resolveForkUsageSnapshot(parentThreadId, boundary) {
|
|
476
|
+
if (!parentThreadId || !boundary) return null;
|
|
477
|
+
const parent = byId.get(parentThreadId);
|
|
478
|
+
if (!parent?.rollout_path) return null;
|
|
479
|
+
|
|
480
|
+
let timelinePromise = timelineCache.get(parentThreadId);
|
|
481
|
+
if (!timelinePromise) {
|
|
482
|
+
timelinePromise = readUsageTimeline(parent.rollout_path);
|
|
483
|
+
timelineCache.set(parentThreadId, timelinePromise);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const timeline = await timelinePromise;
|
|
487
|
+
return selectForkParentUsageEntry(timeline, boundary)?.usage || null;
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function selectForkParentUsageEntry(timeline, {
|
|
492
|
+
startedAtMs = null,
|
|
493
|
+
firstUsageTimestampMs = null,
|
|
494
|
+
} = {}) {
|
|
495
|
+
if (!Array.isArray(timeline) || !timeline.length) return null;
|
|
496
|
+
const startEntry = findUsageEntryAtOrBefore(timeline, startedAtMs);
|
|
497
|
+
const firstUsageEntry = findUsageEntryAtOrBefore(timeline, firstUsageTimestampMs);
|
|
498
|
+
if (startEntry && firstUsageEntry && startEntry.segment_id !== firstUsageEntry.segment_id) {
|
|
499
|
+
return startEntry;
|
|
500
|
+
}
|
|
501
|
+
return firstUsageEntry || startEntry || null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function applyForkUsageCorrections(sessions, resolveForkUsageSnapshot) {
|
|
505
|
+
for (const session of sessions) {
|
|
506
|
+
const parentThreadId = getForkLineageParentThreadId(session);
|
|
507
|
+
if (!parentThreadId) continue;
|
|
508
|
+
if (session._usage_reset_detected) continue;
|
|
509
|
+
// Use the child's first observed token snapshot as the primary fork
|
|
510
|
+
// boundary. On the real fork-heavy March chains, this consistently
|
|
511
|
+
// produced better attribution than started_at: the delay from start to
|
|
512
|
+
// first token_count is usually sub-second, while started_at retains
|
|
513
|
+
// slightly more inherited parent usage. We still pass started_at through
|
|
514
|
+
// to the resolver so a parent reset between fork start and first child
|
|
515
|
+
// stream can fall back to the pre-reset parent segment instead of
|
|
516
|
+
// under-subtracting inherited usage. If the child rollout later resets its
|
|
517
|
+
// token counters, we treat the post-reset segment as the authoritative
|
|
518
|
+
// child-only usage and do not subtract the parent again, because that
|
|
519
|
+
// would double-normalize the same inherited baseline and undercount the
|
|
520
|
+
// child.
|
|
521
|
+
const inheritedUsage = await resolveForkUsageSnapshot(parentThreadId, {
|
|
522
|
+
startedAtMs: session.started_at ? session.started_at * 1000 : null,
|
|
523
|
+
firstUsageTimestampMs: session._first_usage_timestamp_ms || null,
|
|
524
|
+
});
|
|
525
|
+
applyForkUsageCorrection(session, inheritedUsage);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function getForkLineageParentThreadId(session) {
|
|
530
|
+
return session?.forked_from_id || null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function applyForkUsageCorrection(session, inheritedUsage) {
|
|
534
|
+
if (!hasUsageTotals(inheritedUsage)) return false;
|
|
535
|
+
|
|
536
|
+
let changed = false;
|
|
537
|
+
|
|
538
|
+
if (session?.usage_total) {
|
|
539
|
+
session.usage_total = subtractUsageTotals(session.usage_total, inheritedUsage);
|
|
540
|
+
session.tokens_used = session.usage_total.total_tokens || 0;
|
|
541
|
+
changed = true;
|
|
542
|
+
} else if (typeof session?.tokens_used === 'number') {
|
|
543
|
+
session.tokens_used = Math.max(session.tokens_used - (inheritedUsage.total_tokens || 0), 0);
|
|
544
|
+
changed = true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (session._usage_by_day_raw) {
|
|
548
|
+
session._usage_by_day_raw = subtractUsageFromDayBuckets(session._usage_by_day_raw, inheritedUsage);
|
|
549
|
+
changed = true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return changed;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function subtractUsageFromDayBuckets(usageByDay, inheritedUsage) {
|
|
556
|
+
const remaining = {
|
|
557
|
+
input_tokens: inheritedUsage?.input_tokens || 0,
|
|
558
|
+
cached_input_tokens: inheritedUsage?.cached_input_tokens || 0,
|
|
559
|
+
output_tokens: inheritedUsage?.output_tokens || 0,
|
|
560
|
+
reasoning_output_tokens: inheritedUsage?.reasoning_output_tokens || 0,
|
|
561
|
+
total_tokens: inheritedUsage?.total_tokens || 0,
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const next = {};
|
|
565
|
+
for (const dayKey of Object.keys(usageByDay || {}).sort()) {
|
|
566
|
+
const current = usageByDay[dayKey] || {};
|
|
567
|
+
const adjusted = subtractUsageTotals({
|
|
568
|
+
input_tokens: current.input_tokens || 0,
|
|
569
|
+
cached_input_tokens: current.cached_input_tokens || 0,
|
|
570
|
+
output_tokens: current.output_tokens || 0,
|
|
571
|
+
total_tokens: (current.input_tokens || 0) + (current.output_tokens || 0),
|
|
572
|
+
}, remaining);
|
|
573
|
+
|
|
574
|
+
const consumed = subtractUsageTotals({
|
|
575
|
+
input_tokens: current.input_tokens || 0,
|
|
576
|
+
cached_input_tokens: current.cached_input_tokens || 0,
|
|
577
|
+
output_tokens: current.output_tokens || 0,
|
|
578
|
+
total_tokens: (current.input_tokens || 0) + (current.output_tokens || 0),
|
|
579
|
+
}, adjusted);
|
|
580
|
+
|
|
581
|
+
Object.assign(remaining, subtractUsageTotals(remaining, consumed));
|
|
582
|
+
|
|
583
|
+
if (adjusted.input_tokens > 0 || adjusted.cached_input_tokens > 0 || adjusted.output_tokens > 0) {
|
|
584
|
+
next[dayKey] = {
|
|
585
|
+
input_tokens: adjusted.input_tokens,
|
|
586
|
+
cached_input_tokens: adjusted.cached_input_tokens,
|
|
587
|
+
output_tokens: adjusted.output_tokens,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return next;
|
|
593
|
+
}
|
|
594
|
+
|
|
410
595
|
function deriveLiveSortDay(session, toDayKey) {
|
|
411
596
|
if (session.has_usage_by_day && session.usage_by_day?.length) {
|
|
412
597
|
let best = session.usage_by_day[0];
|
|
@@ -706,5 +891,3 @@ function broadcastBootstrap(state) {
|
|
|
706
891
|
};
|
|
707
892
|
broadcastLive(state, 'bootstrap', payload);
|
|
708
893
|
}
|
|
709
|
-
|
|
710
|
-
|
package/server/live-state.js
CHANGED
|
@@ -242,8 +242,7 @@ function applyDailyBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
242
242
|
|
|
243
243
|
if (session.active_by_day) {
|
|
244
244
|
for (const [dayKey, seconds] of Object.entries(session.active_by_day)) {
|
|
245
|
-
|
|
246
|
-
day.elapsed_seconds += seconds || 0;
|
|
245
|
+
addElapsedToDay(dayMap, dayKey, session, seconds || 0);
|
|
247
246
|
dirtySet.add(dayKey);
|
|
248
247
|
}
|
|
249
248
|
}
|
|
@@ -307,6 +306,14 @@ function addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur,
|
|
|
307
306
|
function addPresenceToDay(dayMap, dayKey, session, fraction) {
|
|
308
307
|
const day = ensureDay(dayMap, dayKey);
|
|
309
308
|
if (fraction > 0.001) day.sessions += 1;
|
|
309
|
+
|
|
310
|
+
const familyKey = session.agent_family || 'generic';
|
|
311
|
+
if (!day.by_family[familyKey]) day.by_family[familyKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
312
|
+
if (fraction > 0.001) day.by_family[familyKey].sessions += 1;
|
|
313
|
+
|
|
314
|
+
const repoKey = session.repo_label || 'unknown';
|
|
315
|
+
if (!day.by_repo[repoKey]) day.by_repo[repoKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
316
|
+
if (fraction > 0.001) day.by_repo[repoKey].sessions += 1;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
function addUsageByDay(dayMap, session, dirtySet) {
|
|
@@ -320,6 +327,16 @@ function addUsageByDay(dayMap, session, dirtySet) {
|
|
|
320
327
|
if (!day.by_model[modelKey]) day.by_model[modelKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
321
328
|
day.by_model[modelKey].tokens += usageDay.tokens || 0;
|
|
322
329
|
if (usageDay.cost !== null) day.by_model[modelKey].cost += usageDay.cost;
|
|
330
|
+
|
|
331
|
+
const familyKey = session.agent_family || 'generic';
|
|
332
|
+
if (!day.by_family[familyKey]) day.by_family[familyKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
333
|
+
day.by_family[familyKey].tokens += usageDay.tokens || 0;
|
|
334
|
+
if (usageDay.cost !== null) day.by_family[familyKey].cost += usageDay.cost;
|
|
335
|
+
|
|
336
|
+
const repoKey = session.repo_label || 'unknown';
|
|
337
|
+
if (!day.by_repo[repoKey]) day.by_repo[repoKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
338
|
+
day.by_repo[repoKey].tokens += usageDay.tokens || 0;
|
|
339
|
+
if (usageDay.cost !== null) day.by_repo[repoKey].cost += usageDay.cost;
|
|
323
340
|
dirtySet.add(dayKey);
|
|
324
341
|
}
|
|
325
342
|
}
|
|
@@ -334,15 +351,45 @@ function addToDay(dayMap, dayKey, session, fraction) {
|
|
|
334
351
|
if (!day.by_model[modelKey]) day.by_model[modelKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
335
352
|
day.by_model[modelKey].tokens += (session.tokens_used || 0) * fraction;
|
|
336
353
|
if (session.cost !== null) day.by_model[modelKey].cost += session.cost * fraction;
|
|
354
|
+
|
|
355
|
+
const familyKey = session.agent_family || 'generic';
|
|
356
|
+
if (!day.by_family[familyKey]) day.by_family[familyKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
357
|
+
day.by_family[familyKey].tokens += (session.tokens_used || 0) * fraction;
|
|
358
|
+
if (session.cost !== null) day.by_family[familyKey].cost += session.cost * fraction;
|
|
359
|
+
if (fraction > 0.001) day.by_family[familyKey].sessions += 1;
|
|
360
|
+
|
|
361
|
+
const repoKey = session.repo_label || 'unknown';
|
|
362
|
+
if (!day.by_repo[repoKey]) day.by_repo[repoKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
363
|
+
day.by_repo[repoKey].tokens += (session.tokens_used || 0) * fraction;
|
|
364
|
+
if (session.cost !== null) day.by_repo[repoKey].cost += session.cost * fraction;
|
|
365
|
+
if (fraction > 0.001) day.by_repo[repoKey].sessions += 1;
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
function ensureDay(dayMap, dayKey) {
|
|
340
369
|
if (!dayMap.has(dayKey)) {
|
|
341
|
-
dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {} });
|
|
370
|
+
dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
342
371
|
}
|
|
343
372
|
return dayMap.get(dayKey);
|
|
344
373
|
}
|
|
345
374
|
|
|
375
|
+
function addElapsedToDay(dayMap, dayKey, session, seconds) {
|
|
376
|
+
if (!seconds) return;
|
|
377
|
+
const day = ensureDay(dayMap, dayKey);
|
|
378
|
+
day.elapsed_seconds += seconds;
|
|
379
|
+
|
|
380
|
+
const modelKey = session.model_name || 'unknown';
|
|
381
|
+
if (!day.by_model[modelKey]) day.by_model[modelKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
382
|
+
day.by_model[modelKey].elapsed_seconds += seconds;
|
|
383
|
+
|
|
384
|
+
const familyKey = session.agent_family || 'generic';
|
|
385
|
+
if (!day.by_family[familyKey]) day.by_family[familyKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
386
|
+
day.by_family[familyKey].elapsed_seconds += seconds;
|
|
387
|
+
|
|
388
|
+
const repoKey = session.repo_label || 'unknown';
|
|
389
|
+
if (!day.by_repo[repoKey]) day.by_repo[repoKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
390
|
+
day.by_repo[repoKey].elapsed_seconds += seconds;
|
|
391
|
+
}
|
|
392
|
+
|
|
346
393
|
function ensureHeatmapDay(dayMap, dayKey) {
|
|
347
394
|
if (!dayMap.has(dayKey)) {
|
|
348
395
|
dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
@@ -424,6 +471,8 @@ function serializeDailyEntry(value) {
|
|
|
424
471
|
elapsed_seconds: Math.round(value?.elapsed_seconds || 0),
|
|
425
472
|
sessions: value?.sessions || 0,
|
|
426
473
|
by_model: deepRoundClone(value?.by_model || {}, ['tokens', 'cost', 'elapsed_seconds']),
|
|
474
|
+
by_family: deepRoundClone(value?.by_family || {}, ['tokens', 'cost', 'elapsed_seconds', 'sessions']),
|
|
475
|
+
by_repo: deepRoundClone(value?.by_repo || {}, ['tokens', 'cost', 'elapsed_seconds', 'sessions']),
|
|
427
476
|
approximate: true,
|
|
428
477
|
};
|
|
429
478
|
}
|