codexmeter 1.0.1 → 1.0.3
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 +2 -1
- package/bin/codexmeter.js +1 -0
- package/dist/assets/index-CBnwoOLi.js +117 -0
- package/dist/assets/index-DhDq9dI8.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +5 -2
- package/server/aggregator.js +9 -5
- package/server/export-replay.js +80 -0
- package/server/export-video.js +770 -0
- package/server/index.js +83 -1
- package/server/ingest.js +125 -41
- package/server/live-state.js +9 -6
- package/server/rollout-reader.js +4 -0
- package/src/utils/animationsDefault.js +280 -0
- package/dist/assets/index-DJWqyRDh.css +0 -1
- package/dist/assets/index-DZKogILW.js +0 -113
package/server/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { attachLiveSubscriber, createIngestState, detachLiveSubscriber, restartIngest, runIngest } from './ingest.js';
|
|
4
|
+
import { attachLiveSubscriber, createIngestState, detachLiveSubscriber, getLatestReplay, restartIngest, runIngest } from './ingest.js';
|
|
5
|
+
import { createJobSummary, createVideoExportManager, getActiveVideoExportJob, getVideoExportJob, getVideoExportSupport, startOverviewVideoExport } from './export-video.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
|
|
8
9
|
export function createServer(codexHome, opts = {}) {
|
|
9
10
|
const app = express();
|
|
11
|
+
app.use(express.json());
|
|
10
12
|
const state = createIngestState();
|
|
13
|
+
const exportManager = createVideoExportManager();
|
|
11
14
|
const distDir = path.join(__dirname, '..', 'dist');
|
|
12
15
|
const apiOnly = opts.devApiOnly === true;
|
|
13
16
|
const ingestOpts = { ...opts };
|
|
@@ -77,6 +80,85 @@ export function createServer(codexHome, opts = {}) {
|
|
|
77
80
|
app.get('/api/heatmap', wrap('heatmap'));
|
|
78
81
|
app.get('/api/families', wrap('families'));
|
|
79
82
|
|
|
83
|
+
app.post('/api/export/overview-video', async (req, res) => {
|
|
84
|
+
const activeJob = getActiveVideoExportJob(exportManager);
|
|
85
|
+
if (activeJob) {
|
|
86
|
+
res.status(409).json({ error: 'Another export job is already running.' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const replay = getLatestReplay(state);
|
|
90
|
+
if (!replay) {
|
|
91
|
+
res.status(409).json({ error: 'No completed ingest replay is available yet.' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const appBaseUrl = req.get('x-codexmeter-client-base') || opts.frontendBaseUrl || `${req.protocol}://${req.get('host')}`;
|
|
96
|
+
const settledEnvelope = state.aggregates ? {
|
|
97
|
+
overview: { data: state.aggregates.overview },
|
|
98
|
+
repos: { data: state.aggregates.repos },
|
|
99
|
+
models: { data: state.aggregates.models },
|
|
100
|
+
families: { data: state.aggregates.families },
|
|
101
|
+
daily: { data: state.aggregates.daily },
|
|
102
|
+
heatmap: { data: state.aggregates.heatmap },
|
|
103
|
+
} : null;
|
|
104
|
+
try {
|
|
105
|
+
const job = await startOverviewVideoExport(exportManager, {
|
|
106
|
+
replay,
|
|
107
|
+
settledEnvelope,
|
|
108
|
+
appBaseUrl,
|
|
109
|
+
installPortableBrowser: Boolean(req.body?.install_portable_browser),
|
|
110
|
+
});
|
|
111
|
+
res.status(202).json(createJobSummary(job, `${req.protocol}://${req.get('host')}`));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
res.status(err.statusCode || 500).json({ error: err.message || String(err) });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
app.get('/api/export/active', (req, res) => {
|
|
118
|
+
const job = getActiveVideoExportJob(exportManager);
|
|
119
|
+
if (!job) {
|
|
120
|
+
res.json({ job: null });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
res.json({ job: createJobSummary(job, `${req.protocol}://${req.get('host')}`) });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
app.get('/api/export/support', async (_req, res) => {
|
|
127
|
+
res.json(await getVideoExportSupport());
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.get('/api/export/:jobId/status', (req, res) => {
|
|
131
|
+
const job = getVideoExportJob(exportManager, req.params.jobId);
|
|
132
|
+
if (!job) {
|
|
133
|
+
res.status(404).json({ error: 'Export job not found.' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
res.json(createJobSummary(job, `${req.protocol}://${req.get('host')}`));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.get('/api/export/:jobId/render-data', (req, res) => {
|
|
140
|
+
const job = getVideoExportJob(exportManager, req.params.jobId);
|
|
141
|
+
if (!job) {
|
|
142
|
+
res.status(404).json({ error: 'Export job not found.' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const payload = exportManager.getRenderPayload(req.params.jobId);
|
|
146
|
+
if (!payload) {
|
|
147
|
+
res.status(404).json({ error: 'Export render data not found.' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
res.json(payload);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.get('/api/export/:jobId/file', (req, res) => {
|
|
154
|
+
const job = getVideoExportJob(exportManager, req.params.jobId);
|
|
155
|
+
if (!job || job.status !== 'complete' || !job.output_path) {
|
|
156
|
+
res.status(404).json({ error: 'Export file not ready.' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.download(job.output_path, job.file_name || `codexmeter-overview-${job.id}.mp4`);
|
|
160
|
+
});
|
|
161
|
+
|
|
80
162
|
app.get('/api/sessions', (req, res) => {
|
|
81
163
|
const q = (req.query.q || '').toLowerCase();
|
|
82
164
|
let sessions = state.sessions || [];
|
package/server/ingest.js
CHANGED
|
@@ -9,17 +9,14 @@ import { buildAggregates, buildSessionView } from './aggregator.js';
|
|
|
9
9
|
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
|
+
import { beginReplayCapture, createReplayCaptureState, failReplayCapture, getReplaySnapshot, recordReplayEvent, resetReplayCapture } from './export-replay.js';
|
|
13
|
+
import { OVERVIEW_INGEST_ANIMATION } from '../src/utils/animationsDefault.js';
|
|
12
14
|
|
|
13
15
|
const LIVE_FRAME_INTERVAL_MS = 50;
|
|
14
16
|
const LIVE_DAYS_PER_SECOND = 6;
|
|
15
|
-
const
|
|
17
|
+
const LIVE_OVERVIEW_CADENCE_MS = Math.round(1000 / 10);
|
|
16
18
|
const LIVE_DAY_KEYS_PER_EMIT = 1;
|
|
17
|
-
const
|
|
18
|
-
overview: LIVE_FRAME_INTERVAL_MS * 2,
|
|
19
|
-
rankings: LIVE_FRAME_INTERVAL_MS * 3,
|
|
20
|
-
daily: LIVE_DAY_CADENCE_MS,
|
|
21
|
-
heatmap: LIVE_DAY_CADENCE_MS,
|
|
22
|
-
};
|
|
19
|
+
const LIVE_SESSION_REORDER_BUFFER = 160;
|
|
23
20
|
|
|
24
21
|
export function createIngestState() {
|
|
25
22
|
return {
|
|
@@ -43,9 +40,11 @@ export function createIngestState() {
|
|
|
43
40
|
live_subscribers: new Set(),
|
|
44
41
|
live_pump_timer: null,
|
|
45
42
|
live_pending_patch: createEmptyLivePatch(),
|
|
43
|
+
live_session_buffer: [],
|
|
46
44
|
live_progress_dirty: false,
|
|
47
45
|
live_last_emit_at: 0,
|
|
48
|
-
|
|
46
|
+
live_last_overview_emit_at: 0,
|
|
47
|
+
replay_capture: createReplayCaptureState(),
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -60,6 +59,12 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
60
59
|
state.live_state = createLiveAggregateState(tz);
|
|
61
60
|
state.phase = 'inventory';
|
|
62
61
|
state.percent = 0;
|
|
62
|
+
beginReplayCapture(state.replay_capture, state.ingest_id, {
|
|
63
|
+
ingest_id: state.ingest_id,
|
|
64
|
+
seq: 0,
|
|
65
|
+
progress: progressPayload(state),
|
|
66
|
+
data: buildLiveBootstrap(state.live_state),
|
|
67
|
+
});
|
|
63
68
|
queueLiveProgress(state);
|
|
64
69
|
broadcastBootstrap(state);
|
|
65
70
|
|
|
@@ -100,6 +105,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
100
105
|
usage_total: null,
|
|
101
106
|
usage_by_day: null,
|
|
102
107
|
has_usage_by_day: false,
|
|
108
|
+
live_sort_ts: null,
|
|
103
109
|
active_by_day: null,
|
|
104
110
|
agent_role: t.agent_role,
|
|
105
111
|
agent_nickname: t.agent_nickname,
|
|
@@ -134,7 +140,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
134
140
|
|
|
135
141
|
state.needs_enrichment = candidates.length;
|
|
136
142
|
state.percent = candidates.length > 0 ? 0.08 : 0.90;
|
|
137
|
-
const BATCH_SIZE = opts.batchSize ||
|
|
143
|
+
const BATCH_SIZE = opts.batchSize || 40;
|
|
138
144
|
const ROOT_REFRESH_EVERY = opts.rootRefreshEvery || 1000;
|
|
139
145
|
let lastRootRefreshCount = 0;
|
|
140
146
|
|
|
@@ -166,12 +172,16 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
166
172
|
s.usage_by_day = buildUsageByDayMetrics(s.model_name, data.usage_by_day);
|
|
167
173
|
s.has_usage_by_day = s.usage_by_day.length > 0;
|
|
168
174
|
}
|
|
175
|
+
if (data.first_usage_timestamp) {
|
|
176
|
+
s.live_sort_ts = Math.floor(data.first_usage_timestamp / 1000);
|
|
177
|
+
}
|
|
169
178
|
if (data.active_seconds && data.active_seconds > 0) {
|
|
170
179
|
s.elapsed_seconds = data.active_seconds;
|
|
171
180
|
s.active_by_day = data.active_by_day || null;
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
finalizeSessionMetrics(s, toDayKey);
|
|
184
|
+
s.live_sort_day = deriveLiveSortDay(s, toDayKey);
|
|
175
185
|
s.materialized = true;
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -183,8 +193,14 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
183
193
|
assignRootThreadIds(sessions);
|
|
184
194
|
lastRootRefreshCount = state.enriched;
|
|
185
195
|
}
|
|
196
|
+
const liveReady = bufferLiveSessionsForPresentation(state, batch);
|
|
197
|
+
if (state.live_session_buffer[0]?.live_sort_day) {
|
|
198
|
+
state.current_date_bucket = state.live_session_buffer[0].live_sort_day;
|
|
199
|
+
} else if (liveReady[0]?.live_sort_day) {
|
|
200
|
+
state.current_date_bucket = liveReady[0].live_sort_day;
|
|
201
|
+
}
|
|
186
202
|
const livePatch = createEmptyLivePatch();
|
|
187
|
-
for (const session of
|
|
203
|
+
for (const session of liveReady) {
|
|
188
204
|
applySessionToLiveState(state.live_state, session, livePatch);
|
|
189
205
|
}
|
|
190
206
|
queueLivePatch(state, livePatch);
|
|
@@ -203,6 +219,11 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
assignRootThreadIds(sessions);
|
|
222
|
+
const finalLivePatch = createEmptyLivePatch();
|
|
223
|
+
for (const session of flushBufferedLiveSessions(state)) {
|
|
224
|
+
applySessionToLiveState(state.live_state, session, finalLivePatch);
|
|
225
|
+
}
|
|
226
|
+
queueLivePatch(state, finalLivePatch);
|
|
206
227
|
|
|
207
228
|
state.phase = 'aggregation';
|
|
208
229
|
state.percent = state.needs_enrichment > 0 ? 0.95 : 0.90;
|
|
@@ -222,6 +243,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
222
243
|
if (!isCurrentRun()) return;
|
|
223
244
|
state.error = err.message;
|
|
224
245
|
state.phase = 'error';
|
|
246
|
+
failReplayCapture(state.replay_capture);
|
|
225
247
|
console.error('Ingest error:', err);
|
|
226
248
|
flushLive(state, 'ingest-error');
|
|
227
249
|
} finally {
|
|
@@ -253,9 +275,11 @@ export function restartIngest(codexHome, state, opts = {}) {
|
|
|
253
275
|
state.live_state = null;
|
|
254
276
|
state.live_seq = 0;
|
|
255
277
|
state.live_pending_patch = createEmptyLivePatch();
|
|
278
|
+
state.live_session_buffer = [];
|
|
256
279
|
state.live_progress_dirty = false;
|
|
257
280
|
state.live_last_emit_at = 0;
|
|
258
|
-
state.
|
|
281
|
+
state.live_last_overview_emit_at = 0;
|
|
282
|
+
resetReplayCapture(state.replay_capture);
|
|
259
283
|
|
|
260
284
|
return runIngest(codexHome, state, opts);
|
|
261
285
|
}
|
|
@@ -373,6 +397,49 @@ function buildUsageByDayMetrics(modelName, usageByDay) {
|
|
|
373
397
|
return entries;
|
|
374
398
|
}
|
|
375
399
|
|
|
400
|
+
function deriveLiveSortDay(session, toDayKey) {
|
|
401
|
+
if (session.has_usage_by_day && session.usage_by_day?.length) {
|
|
402
|
+
let best = session.usage_by_day[0];
|
|
403
|
+
for (const entry of session.usage_by_day) {
|
|
404
|
+
if ((entry.tokens || 0) > (best.tokens || 0)) {
|
|
405
|
+
best = entry;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if ((entry.tokens || 0) === (best.tokens || 0) && String(entry.day) > String(best.day)) {
|
|
409
|
+
best = entry;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return best?.day || null;
|
|
413
|
+
}
|
|
414
|
+
if (session.started_at) {
|
|
415
|
+
return toDayKey(session.started_at * 1000);
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function compareLiveSessionOrder(a, b) {
|
|
421
|
+
const dayCompare = String(a.live_sort_day || '9999-12-31').localeCompare(String(b.live_sort_day || '9999-12-31'));
|
|
422
|
+
if (dayCompare !== 0) return dayCompare;
|
|
423
|
+
const liveTsCompare = (a.live_sort_ts || a.started_at || 0) - (b.live_sort_ts || b.started_at || 0);
|
|
424
|
+
if (liveTsCompare !== 0) return liveTsCompare;
|
|
425
|
+
return String(a.thread_id || '').localeCompare(String(b.thread_id || ''));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function bufferLiveSessionsForPresentation(state, sessions) {
|
|
429
|
+
if (!sessions.length) return [];
|
|
430
|
+
state.live_session_buffer.push(...sessions);
|
|
431
|
+
state.live_session_buffer.sort(compareLiveSessionOrder);
|
|
432
|
+
const flushCount = Math.max(0, state.live_session_buffer.length - LIVE_SESSION_REORDER_BUFFER);
|
|
433
|
+
if (flushCount <= 0) return [];
|
|
434
|
+
return state.live_session_buffer.splice(0, flushCount);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function flushBufferedLiveSessions(state) {
|
|
438
|
+
if (!state.live_session_buffer.length) return [];
|
|
439
|
+
state.live_session_buffer.sort(compareLiveSessionOrder);
|
|
440
|
+
return state.live_session_buffer.splice(0, state.live_session_buffer.length);
|
|
441
|
+
}
|
|
442
|
+
|
|
376
443
|
function queueLiveProgress(state) {
|
|
377
444
|
state.live_progress_dirty = true;
|
|
378
445
|
ensureLivePump(state);
|
|
@@ -385,13 +452,14 @@ function queueLivePatch(state, patch) {
|
|
|
385
452
|
}
|
|
386
453
|
|
|
387
454
|
function ensureLivePump(state) {
|
|
388
|
-
if (state.live_pump_timer
|
|
455
|
+
if (state.live_pump_timer) return;
|
|
456
|
+
if (!state.live_subscribers.size && !state.replay_capture.active) return;
|
|
389
457
|
state.live_pump_timer = setInterval(() => flushLive(state), LIVE_FRAME_INTERVAL_MS);
|
|
390
458
|
}
|
|
391
459
|
|
|
392
460
|
function stopLivePumpIfIdle(state) {
|
|
393
461
|
if (!state.live_pump_timer) return;
|
|
394
|
-
if (state.live_subscribers.size) return;
|
|
462
|
+
if (state.live_subscribers.size || state.replay_capture.active) return;
|
|
395
463
|
clearInterval(state.live_pump_timer);
|
|
396
464
|
state.live_pump_timer = null;
|
|
397
465
|
finalizeWithoutSubscribers(state);
|
|
@@ -399,6 +467,7 @@ function stopLivePumpIfIdle(state) {
|
|
|
399
467
|
|
|
400
468
|
function finalizeWithoutSubscribers(state) {
|
|
401
469
|
if (state.live_subscribers.size) return;
|
|
470
|
+
if (state.replay_capture.active) return;
|
|
402
471
|
if (!state.presentation_complete_pending) return;
|
|
403
472
|
state.phase = 'complete';
|
|
404
473
|
state.percent = 1;
|
|
@@ -409,7 +478,7 @@ function finalizeWithoutSubscribers(state) {
|
|
|
409
478
|
}
|
|
410
479
|
|
|
411
480
|
function flushLive(state, forcedEvent = null) {
|
|
412
|
-
if (!state.live_subscribers.size) {
|
|
481
|
+
if (!state.live_subscribers.size && !state.replay_capture.active) {
|
|
413
482
|
state.live_pending_patch = createEmptyLivePatch();
|
|
414
483
|
state.live_progress_dirty = false;
|
|
415
484
|
stopLivePumpIfIdle(state);
|
|
@@ -449,12 +518,16 @@ function flushLive(state, forcedEvent = null) {
|
|
|
449
518
|
payload.data = buildLiveBootstrap(state.live_state);
|
|
450
519
|
}
|
|
451
520
|
|
|
452
|
-
|
|
521
|
+
recordReplayEvent(state.replay_capture, event, payload);
|
|
522
|
+
if (state.live_subscribers.size) {
|
|
523
|
+
broadcastLive(state, event, payload);
|
|
524
|
+
}
|
|
453
525
|
state.live_last_emit_at = Date.now();
|
|
454
526
|
state.live_progress_dirty = shouldEmitComplete ? false : (event === 'progress' ? false : state.live_progress_dirty);
|
|
455
527
|
if (forcedEvent) {
|
|
456
528
|
state.live_pending_patch = createEmptyLivePatch();
|
|
457
529
|
}
|
|
530
|
+
stopLivePumpIfIdle(state);
|
|
458
531
|
}
|
|
459
532
|
|
|
460
533
|
function broadcastLive(state, event, payload) {
|
|
@@ -496,41 +569,48 @@ function takeFlushablePatch(state) {
|
|
|
496
569
|
const now = Date.now();
|
|
497
570
|
const sent = createEmptyLivePatch();
|
|
498
571
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
state.live_last_surface_emit_at.overview = now;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const rankingsDirty =
|
|
572
|
+
const overviewDirty =
|
|
573
|
+
state.live_pending_patch.overview.size > 0 ||
|
|
505
574
|
state.live_pending_patch.repos.total.size > 0 || state.live_pending_patch.repos.d7.size > 0 || state.live_pending_patch.repos.d30.size > 0 ||
|
|
506
575
|
state.live_pending_patch.models.total.size > 0 || state.live_pending_patch.models.d7.size > 0 || state.live_pending_patch.models.d30.size > 0 ||
|
|
507
|
-
state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0
|
|
576
|
+
state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0 ||
|
|
577
|
+
state.live_pending_patch.daily.size > 0 || state.live_pending_patch.heatmap.size > 0;
|
|
508
578
|
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
moveRangeSets(state.live_pending_patch.models, sent.models);
|
|
512
|
-
moveRangeSets(state.live_pending_patch.families, sent.families);
|
|
513
|
-
state.live_last_surface_emit_at.rankings = now;
|
|
579
|
+
if (!overviewDirty || !readyForOverview(state, now)) {
|
|
580
|
+
return sent;
|
|
514
581
|
}
|
|
515
582
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
583
|
+
moveSet(state.live_pending_patch.overview, sent.overview);
|
|
584
|
+
moveRangeSets(state.live_pending_patch.repos, sent.repos);
|
|
585
|
+
moveRangeSets(state.live_pending_patch.models, sent.models);
|
|
586
|
+
moveRangeSets(state.live_pending_patch.families, sent.families);
|
|
587
|
+
|
|
588
|
+
const nextDayKeys = takeNextChronologicalDayKeys(
|
|
589
|
+
state.live_pending_patch.daily,
|
|
590
|
+
state.live_pending_patch.heatmap,
|
|
591
|
+
LIVE_DAY_KEYS_PER_EMIT
|
|
592
|
+
);
|
|
593
|
+
moveSpecificKeys(state.live_pending_patch.daily, sent.daily, nextDayKeys);
|
|
594
|
+
moveSpecificKeys(state.live_pending_patch.heatmap, sent.heatmap, nextDayKeys);
|
|
595
|
+
state.live_last_overview_emit_at = now;
|
|
528
596
|
|
|
529
597
|
return sent;
|
|
530
598
|
}
|
|
531
599
|
|
|
532
|
-
function
|
|
533
|
-
return (now - state.
|
|
600
|
+
function readyForOverview(state, now) {
|
|
601
|
+
return (now - state.live_last_overview_emit_at) >= getOverviewCadenceMs(state);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function getOverviewCadenceMs(state) {
|
|
605
|
+
const tail = OVERVIEW_INGEST_ANIMATION.tail;
|
|
606
|
+
const tailStartPercent = Math.min(Math.max(tail?.startPercent ?? 0.95, 0), 0.999);
|
|
607
|
+
const tailHz = Number.isFinite(tail?.overviewHz) && tail.overviewHz > 0 ? tail.overviewHz : 5;
|
|
608
|
+
const tailCadenceMs = Math.round(1000 / tailHz);
|
|
609
|
+
|
|
610
|
+
if (tail?.enabled && (state.presentation_complete_pending || (state.percent || 0) >= tailStartPercent)) {
|
|
611
|
+
return tailCadenceMs;
|
|
612
|
+
}
|
|
613
|
+
return LIVE_OVERVIEW_CADENCE_MS;
|
|
534
614
|
}
|
|
535
615
|
|
|
536
616
|
function moveSet(from, to) {
|
|
@@ -602,6 +682,10 @@ export function detachLiveSubscriber(state, res) {
|
|
|
602
682
|
stopLivePumpIfIdle(state);
|
|
603
683
|
}
|
|
604
684
|
|
|
685
|
+
export function getLatestReplay(state) {
|
|
686
|
+
return getReplaySnapshot(state.replay_capture);
|
|
687
|
+
}
|
|
688
|
+
|
|
605
689
|
function broadcastBootstrap(state) {
|
|
606
690
|
if (!state.live_subscribers.size) return;
|
|
607
691
|
const payload = {
|
package/server/live-state.js
CHANGED
|
@@ -82,7 +82,7 @@ export function buildLiveBootstrap(live) {
|
|
|
82
82
|
export function buildLivePatch(live, patch) {
|
|
83
83
|
return {
|
|
84
84
|
overview: Object.fromEntries(
|
|
85
|
-
[...patch.overview].map((rangeKey) => [rangeKey, serializeOverviewBucket(live.overview[rangeKey])])
|
|
85
|
+
[...patch.overview].map((rangeKey) => [rangeKey, serializeOverviewBucket(live.overview[rangeKey], live.lowerBounds[rangeKey])])
|
|
86
86
|
),
|
|
87
87
|
repos: serializePatchedTopRanges(live.repos, live.repoTopKeys, patch.repos, serializeRepoSummary),
|
|
88
88
|
models: serializePatchedTopRanges(live.models, live.modelTopKeys, patch.models, serializeModelSummary),
|
|
@@ -352,14 +352,17 @@ function ensureHeatmapDay(dayMap, dayKey) {
|
|
|
352
352
|
|
|
353
353
|
function serializeOverview(live) {
|
|
354
354
|
return {
|
|
355
|
-
total: serializeOverviewBucket(live.overview.total),
|
|
356
|
-
d7: serializeOverviewBucket(live.overview.d7),
|
|
357
|
-
d30: serializeOverviewBucket(live.overview.d30),
|
|
355
|
+
total: serializeOverviewBucket(live.overview.total, live.lowerBounds.total),
|
|
356
|
+
d7: serializeOverviewBucket(live.overview.d7, live.lowerBounds.d7),
|
|
357
|
+
d30: serializeOverviewBucket(live.overview.d30, live.lowerBounds.d30),
|
|
358
358
|
cost_assumptions: CACHE_ASSUMPTIONS,
|
|
359
359
|
};
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
function serializeOverviewBucket(bucket) {
|
|
362
|
+
function serializeOverviewBucket(bucket, lowerBound = 0) {
|
|
363
|
+
const boundedFrom = bucket.earliest === Infinity
|
|
364
|
+
? null
|
|
365
|
+
: Math.max(bucket.earliest, lowerBound || 0);
|
|
363
366
|
return {
|
|
364
367
|
total_tokens: bucket.total_tokens,
|
|
365
368
|
total_cost: bucket.total_cost,
|
|
@@ -368,7 +371,7 @@ function serializeOverviewBucket(bucket) {
|
|
|
368
371
|
active_models: bucket.modelSet.size,
|
|
369
372
|
total_elapsed_seconds: bucket.total_elapsed_seconds,
|
|
370
373
|
date_range: {
|
|
371
|
-
from:
|
|
374
|
+
from: boundedFrom,
|
|
372
375
|
to: bucket.latest === -Infinity ? null : bucket.latest,
|
|
373
376
|
},
|
|
374
377
|
coverage: {
|
package/server/rollout-reader.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
15
15
|
model_name: null,
|
|
16
16
|
reasoning_effort: null,
|
|
17
17
|
first_timestamp: null,
|
|
18
|
+
first_usage_timestamp: null,
|
|
18
19
|
last_timestamp: null,
|
|
19
20
|
active_seconds: null,
|
|
20
21
|
active_by_day: null,
|
|
@@ -97,6 +98,9 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
97
98
|
if (obj.timestamp && hasUsage(usageDelta)) {
|
|
98
99
|
const ts = new Date(obj.timestamp).getTime();
|
|
99
100
|
if (!isNaN(ts)) {
|
|
101
|
+
if (!result.first_usage_timestamp || ts < result.first_usage_timestamp) {
|
|
102
|
+
result.first_usage_timestamp = ts;
|
|
103
|
+
}
|
|
100
104
|
const dayKey = toDayKey(ts);
|
|
101
105
|
mergeUsageTotals(usageByDay, dayKey, usageDelta);
|
|
102
106
|
}
|