codexmeter 1.0.23 → 1.0.24
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/package.json +1 -1
- package/server/aggregator.js +55 -37
- package/server/ingest.js +58 -9
- package/server/rollout-reader.js +41 -9
- package/server/sqlite-reader.js +17 -6
package/package.json
CHANGED
package/server/aggregator.js
CHANGED
|
@@ -11,7 +11,16 @@ function normalizeEffortKey(effort) {
|
|
|
11
11
|
return k || 'unknown';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function modelKeyFor(session, opts = {}) {
|
|
15
|
+
const key = session.model_name || 'unknown';
|
|
16
|
+
if (key === 'unknown' && opts.includeUnknownModels === false) return null;
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildAggregates(sessions, tz, sessionView = null, opts = {}) {
|
|
21
|
+
const aggregateOpts = {
|
|
22
|
+
includeUnknownModels: opts.includeUnknownModels !== false,
|
|
23
|
+
};
|
|
15
24
|
const now = Date.now() / 1000;
|
|
16
25
|
const d7 = now - 7 * 86400;
|
|
17
26
|
const d30 = now - 30 * 86400;
|
|
@@ -25,21 +34,21 @@ export function buildAggregates(sessions, tz, sessionView = null) {
|
|
|
25
34
|
|
|
26
35
|
const overview = buildOverview(sessions, groupedSessions, d7, d30);
|
|
27
36
|
const repos = {
|
|
28
|
-
total: buildRepos(rawBuckets.total),
|
|
29
|
-
d7: buildRepos(rawBuckets.d7),
|
|
30
|
-
d30: buildRepos(rawBuckets.d30),
|
|
37
|
+
total: buildRepos(rawBuckets.total, aggregateOpts),
|
|
38
|
+
d7: buildRepos(rawBuckets.d7, aggregateOpts),
|
|
39
|
+
d30: buildRepos(rawBuckets.d30, aggregateOpts),
|
|
31
40
|
};
|
|
32
41
|
const models = {
|
|
33
|
-
total: buildModels(rawBuckets.total),
|
|
34
|
-
d7: buildModels(rawBuckets.d7),
|
|
35
|
-
d30: buildModels(rawBuckets.d30),
|
|
42
|
+
total: buildModels(rawBuckets.total, aggregateOpts),
|
|
43
|
+
d7: buildModels(rawBuckets.d7, aggregateOpts),
|
|
44
|
+
d30: buildModels(rawBuckets.d30, aggregateOpts),
|
|
36
45
|
};
|
|
37
46
|
const families = {
|
|
38
47
|
total: buildFamilies(rawBuckets.total),
|
|
39
48
|
d7: buildFamilies(rawBuckets.d7),
|
|
40
49
|
d30: buildFamilies(rawBuckets.d30),
|
|
41
50
|
};
|
|
42
|
-
const daily = buildDaily(sessions, groupedSessions, tz);
|
|
51
|
+
const daily = buildDaily(sessions, groupedSessions, tz, aggregateOpts);
|
|
43
52
|
const heatmap = buildHeatmap(sessions, groupedSessions, tz);
|
|
44
53
|
|
|
45
54
|
return { overview, repos, models, daily, heatmap, families };
|
|
@@ -135,7 +144,7 @@ function buildOverview(rawSessions, groupedSessions, d7, d30) {
|
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
|
|
138
|
-
function buildRepos(sessions) {
|
|
147
|
+
function buildRepos(sessions, opts) {
|
|
139
148
|
const map = new Map();
|
|
140
149
|
for (const s of sessions) {
|
|
141
150
|
const key = s.repo_label;
|
|
@@ -163,15 +172,17 @@ function buildRepos(sessions) {
|
|
|
163
172
|
}
|
|
164
173
|
r.sessions++;
|
|
165
174
|
|
|
166
|
-
const mKey = s
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
const mKey = modelKeyFor(s, opts);
|
|
176
|
+
if (mKey) {
|
|
177
|
+
if (!r.by_model[mKey]) r.by_model[mKey] = { tokens: 0, cost: 0, sessions: 0, exact_priced: 0, heuristic_priced: 0 };
|
|
178
|
+
r.by_model[mKey].tokens += s.tokens_used;
|
|
179
|
+
if (s.cost !== null) {
|
|
180
|
+
r.by_model[mKey].cost += s.cost;
|
|
181
|
+
if (s.cost_source === 'exact') r.by_model[mKey].exact_priced++;
|
|
182
|
+
if (s.cost_source === 'heuristic') r.by_model[mKey].heuristic_priced++;
|
|
183
|
+
}
|
|
184
|
+
r.by_model[mKey].sessions++;
|
|
173
185
|
}
|
|
174
|
-
r.by_model[mKey].sessions++;
|
|
175
186
|
|
|
176
187
|
const fKey = s.agent_family;
|
|
177
188
|
if (!r.by_family[fKey]) r.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0, exact_priced: 0, heuristic_priced: 0 };
|
|
@@ -186,10 +197,11 @@ function buildRepos(sessions) {
|
|
|
186
197
|
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
187
198
|
}
|
|
188
199
|
|
|
189
|
-
function buildModels(sessions) {
|
|
200
|
+
function buildModels(sessions, opts) {
|
|
190
201
|
const map = new Map();
|
|
191
202
|
for (const s of sessions) {
|
|
192
|
-
const key = s
|
|
203
|
+
const key = modelKeyFor(s, opts);
|
|
204
|
+
if (!key) continue;
|
|
193
205
|
if (!map.has(key)) map.set(key, { model_name: key, tokens: 0, cost: 0, cost_known: 0, exact_priced: 0, heuristic_priced: 0, sessions: 0, by_effort: {} });
|
|
194
206
|
const m = map.get(key);
|
|
195
207
|
m.tokens += s.tokens_used;
|
|
@@ -230,7 +242,7 @@ function buildFamilies(sessions) {
|
|
|
230
242
|
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
231
243
|
}
|
|
232
244
|
|
|
233
|
-
function buildDaily(rawSessions, groupedSessions, tz) {
|
|
245
|
+
function buildDaily(rawSessions, groupedSessions, tz, opts) {
|
|
234
246
|
const dayMap = new Map();
|
|
235
247
|
for (const s of rawSessions) {
|
|
236
248
|
if (!s.started_at || !s.ended_at) continue;
|
|
@@ -242,10 +254,10 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
242
254
|
|
|
243
255
|
if (s.has_usage_by_day) {
|
|
244
256
|
addSessionPresence(dayMap, startMs, endMs, totalDur, tz, s);
|
|
245
|
-
addUsageByDay(dayMap, s);
|
|
257
|
+
addUsageByDay(dayMap, s, opts);
|
|
246
258
|
} else {
|
|
247
259
|
for (const { dayKey, overlapMs } of splitIntervalByDay(startMs, endMs, tz)) {
|
|
248
|
-
addToDay(dayMap, dayKey, s, overlapMs / totalDur);
|
|
260
|
+
addToDay(dayMap, dayKey, s, overlapMs / totalDur, opts);
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
}
|
|
@@ -253,7 +265,7 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
253
265
|
for (const s of rawSessions) {
|
|
254
266
|
if (!s.active_by_day) continue;
|
|
255
267
|
for (const [dayKey, seconds] of Object.entries(s.active_by_day)) {
|
|
256
|
-
addElapsedToDay(dayMap, dayKey, s, seconds || 0);
|
|
268
|
+
addElapsedToDay(dayMap, dayKey, s, seconds || 0, opts);
|
|
257
269
|
}
|
|
258
270
|
}
|
|
259
271
|
|
|
@@ -278,17 +290,19 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
278
290
|
}));
|
|
279
291
|
}
|
|
280
292
|
|
|
281
|
-
function addToDay(dayMap, dayKey, session, fraction) {
|
|
293
|
+
function addToDay(dayMap, dayKey, session, fraction, opts) {
|
|
282
294
|
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
283
295
|
const d = dayMap.get(dayKey);
|
|
284
296
|
d.tokens += session.tokens_used * fraction;
|
|
285
297
|
if (session.cost !== null) d.cost += session.cost * fraction;
|
|
286
298
|
if (fraction > 0.001) d.sessions++;
|
|
287
299
|
|
|
288
|
-
const mKey = session
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
300
|
+
const mKey = modelKeyFor(session, opts);
|
|
301
|
+
if (mKey) {
|
|
302
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
303
|
+
d.by_model[mKey].tokens += session.tokens_used * fraction;
|
|
304
|
+
if (session.cost !== null) d.by_model[mKey].cost += session.cost * fraction;
|
|
305
|
+
}
|
|
292
306
|
|
|
293
307
|
const fKey = session.agent_family;
|
|
294
308
|
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
@@ -380,7 +394,7 @@ function addPresenceToDay(dayMap, dayKey, session, fraction) {
|
|
|
380
394
|
if (fraction > 0.001) d.by_repo[rKey].sessions++;
|
|
381
395
|
}
|
|
382
396
|
|
|
383
|
-
function addUsageByDay(dayMap, session) {
|
|
397
|
+
function addUsageByDay(dayMap, session, opts) {
|
|
384
398
|
for (const usageDay of session.usage_by_day || []) {
|
|
385
399
|
const dayKey = usageDay.day;
|
|
386
400
|
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
@@ -388,10 +402,12 @@ function addUsageByDay(dayMap, session) {
|
|
|
388
402
|
d.tokens += usageDay.tokens || 0;
|
|
389
403
|
if (usageDay.cost !== null) d.cost += usageDay.cost;
|
|
390
404
|
|
|
391
|
-
const mKey = session
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
405
|
+
const mKey = modelKeyFor(session, opts);
|
|
406
|
+
if (mKey) {
|
|
407
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
408
|
+
d.by_model[mKey].tokens += usageDay.tokens || 0;
|
|
409
|
+
if (usageDay.cost !== null) d.by_model[mKey].cost += usageDay.cost;
|
|
410
|
+
}
|
|
395
411
|
|
|
396
412
|
const fKey = session.agent_family;
|
|
397
413
|
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
|
@@ -405,15 +421,17 @@ function addUsageByDay(dayMap, session) {
|
|
|
405
421
|
}
|
|
406
422
|
}
|
|
407
423
|
|
|
408
|
-
function addElapsedToDay(dayMap, dayKey, session, seconds) {
|
|
424
|
+
function addElapsedToDay(dayMap, dayKey, session, seconds, opts) {
|
|
409
425
|
if (!seconds) return;
|
|
410
426
|
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
411
427
|
const d = dayMap.get(dayKey);
|
|
412
428
|
d.elapsed_seconds += seconds;
|
|
413
429
|
|
|
414
|
-
const mKey = session
|
|
415
|
-
if (
|
|
416
|
-
|
|
430
|
+
const mKey = modelKeyFor(session, opts);
|
|
431
|
+
if (mKey) {
|
|
432
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
433
|
+
d.by_model[mKey].elapsed_seconds += seconds;
|
|
434
|
+
}
|
|
417
435
|
|
|
418
436
|
const fKey = session.agent_family || 'generic';
|
|
419
437
|
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0 };
|
package/server/ingest.js
CHANGED
|
@@ -132,7 +132,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
132
132
|
agent_nickname: t.agent_nickname,
|
|
133
133
|
agent_family: classifyAgentFamily(agentRole),
|
|
134
134
|
is_subagent: isSubagent(agentRole),
|
|
135
|
-
parent_thread_id: null,
|
|
135
|
+
parent_thread_id: t.parent_thread_id || null,
|
|
136
136
|
forked_from_id: null,
|
|
137
137
|
cost: null,
|
|
138
138
|
cost_source: 'unavailable',
|
|
@@ -142,6 +142,9 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
const sessionById = new Map(sessions.map((session) => [session.thread_id, session]));
|
|
146
|
+
inheritMissingLineageModels(sessions, sessionById);
|
|
147
|
+
|
|
145
148
|
state.phase = 'enrichment';
|
|
146
149
|
queueLiveProgress(state);
|
|
147
150
|
|
|
@@ -226,6 +229,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
inheritMissingLineageModels(batch, sessionById);
|
|
229
233
|
await applyForkUsageCorrections(batch, resolveForkUsageSnapshot, FORK_CORRECTION_CONCURRENCY);
|
|
230
234
|
for (const s of batch) {
|
|
231
235
|
if (s._usage_by_day_raw) {
|
|
@@ -301,6 +305,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
301
305
|
}
|
|
302
306
|
}
|
|
303
307
|
|
|
308
|
+
inheritMissingLineageModels(sessions, sessionById);
|
|
304
309
|
for (const s of sessions) {
|
|
305
310
|
finalizeSessionMetrics(s, toDayKey);
|
|
306
311
|
}
|
|
@@ -378,26 +383,43 @@ export function restartIngest(codexHome, state, opts = {}) {
|
|
|
378
383
|
}
|
|
379
384
|
|
|
380
385
|
function rebuildAggregates(sessions, state, opts, tz, mode = {}) {
|
|
381
|
-
const source = mode.partial ? sessions.filter(
|
|
386
|
+
const source = mode.partial ? sessions.filter(isSafeForPartialAggregation) : sessions;
|
|
382
387
|
const visibleSource = filterVisibleSessions(source);
|
|
383
388
|
const filtered = applyFilters(visibleSource, opts);
|
|
384
389
|
const sessionView = buildSessionView(filtered, source);
|
|
385
390
|
state.sessions = sessionView;
|
|
386
|
-
state.aggregates = buildAggregates(filtered, tz, sessionView
|
|
391
|
+
state.aggregates = buildAggregates(filtered, tz, sessionView, {
|
|
392
|
+
includeUnknownModels: mode.includeUnknownModels !== false,
|
|
393
|
+
});
|
|
387
394
|
state.generated_at = new Date().toISOString();
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
function publishPartialAggregates(sessions, state, opts, tz, toDayKey) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
rebuildAggregates(sessions, state, opts, tz);
|
|
398
|
+
const partialSessions = createPartialAggregateSessions(sessions, toDayKey);
|
|
399
|
+
rebuildAggregates(partialSessions, state, opts, tz, {
|
|
400
|
+
includeUnknownModels: false,
|
|
401
|
+
});
|
|
396
402
|
state.partial_ready = true;
|
|
397
403
|
state.percent = Math.max(state.percent, 0.081);
|
|
398
404
|
queueLiveProgress(state);
|
|
399
405
|
}
|
|
400
406
|
|
|
407
|
+
export function createPartialAggregateSessions(sessions, toDayKey) {
|
|
408
|
+
assignRootThreadIds(sessions, toDayKey);
|
|
409
|
+
return sessions
|
|
410
|
+
.filter(isSafeForPartialAggregation)
|
|
411
|
+
.map((session) => {
|
|
412
|
+
const clone = { ...session };
|
|
413
|
+
finalizeSessionMetrics(clone, toDayKey);
|
|
414
|
+
return clone;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function isSafeForPartialAggregation(session) {
|
|
419
|
+
if (!session?.rollout_path || session.materialized) return true;
|
|
420
|
+
return !getForkLineageParentThreadId(session);
|
|
421
|
+
}
|
|
422
|
+
|
|
401
423
|
export function filterVisibleSessions(sessions) {
|
|
402
424
|
return sessions.filter((session) => !isReviewLauncherSession(session));
|
|
403
425
|
}
|
|
@@ -429,6 +451,33 @@ export function selectEnrichmentCandidates(sessions, opts = {}) {
|
|
|
429
451
|
return [...warmup, ...recent, ...older];
|
|
430
452
|
}
|
|
431
453
|
|
|
454
|
+
export function inheritMissingLineageModels(targets, sessionById = new Map(targets.map((session) => [session.thread_id, session]))) {
|
|
455
|
+
const memo = new Map();
|
|
456
|
+
const resolving = new Set();
|
|
457
|
+
|
|
458
|
+
const resolveModel = (session) => {
|
|
459
|
+
if (!session) return null;
|
|
460
|
+
if (session.model_name) return session.model_name;
|
|
461
|
+
if (memo.has(session.thread_id)) return memo.get(session.thread_id);
|
|
462
|
+
if (resolving.has(session.thread_id)) return null;
|
|
463
|
+
|
|
464
|
+
resolving.add(session.thread_id);
|
|
465
|
+
const parent = sessionById.get(getForkLineageParentThreadId(session));
|
|
466
|
+
const modelName = resolveModel(parent);
|
|
467
|
+
if (modelName && !session.model_name) {
|
|
468
|
+
session.model_name = modelName;
|
|
469
|
+
}
|
|
470
|
+
resolving.delete(session.thread_id);
|
|
471
|
+
|
|
472
|
+
memo.set(session.thread_id, session.model_name || null);
|
|
473
|
+
return memo.get(session.thread_id);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
for (const session of targets) {
|
|
477
|
+
resolveModel(session);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
432
481
|
export function pickVisibleDateBucket(bufferedSessions, liveReadySessions) {
|
|
433
482
|
return bufferedSessions[0]?.live_sort_day || liveReadySessions[0]?.live_sort_day || null;
|
|
434
483
|
}
|
|
@@ -636,7 +685,7 @@ async function applyForkUsageCorrections(sessions, resolveForkUsageSnapshot, con
|
|
|
636
685
|
}
|
|
637
686
|
|
|
638
687
|
export function getForkLineageParentThreadId(session) {
|
|
639
|
-
return session?.forked_from_id || null;
|
|
688
|
+
return session?.forked_from_id || session?.parent_thread_id || null;
|
|
640
689
|
}
|
|
641
690
|
|
|
642
691
|
export function applyForkUsageCorrection(session, inheritedUsage) {
|
package/server/rollout-reader.js
CHANGED
|
@@ -14,6 +14,7 @@ const RG_RELEVANT_PATTERN =
|
|
|
14
14
|
const RG_TOKEN_COUNT_PATTERN = '"type"\\s*:\\s*"token_count"';
|
|
15
15
|
const RG_TIMESTAMP_PATTERN = '^\\{[^{}]*"timestamp"\\s*:\\s*"[^"]+"';
|
|
16
16
|
const RG_ANY_TIMESTAMP_PATTERN = '"timestamp"\\s*:\\s*"[^"]+"';
|
|
17
|
+
const SORTED_BY_TIMESTAMP = Symbol('codexmeter.sortedByTimestamp');
|
|
17
18
|
|
|
18
19
|
export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
19
20
|
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
@@ -266,6 +267,7 @@ export async function readUsageTimeline(rolloutPath, opts = {}) {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
timeline.sort((left, right) => left.timestamp - right.timestamp);
|
|
270
|
+
markTimelineSorted(timeline);
|
|
269
271
|
return timeline;
|
|
270
272
|
} catch {
|
|
271
273
|
return [];
|
|
@@ -273,25 +275,55 @@ export async function readUsageTimeline(rolloutPath, opts = {}) {
|
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
export function findUsageEntryAtOrBefore(timeline, timestampMs) {
|
|
276
|
-
if (!Array.isArray(timeline) || !timeline.length ||
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
if (!Array.isArray(timeline) || !timeline.length || timestampMs == null) return null;
|
|
279
|
+
const target = Number(timestampMs);
|
|
280
|
+
if (!Number.isFinite(target)) return null;
|
|
281
|
+
|
|
282
|
+
const orderedTimeline = getTimestampOrderedTimeline(timeline);
|
|
283
|
+
let left = 0;
|
|
284
|
+
let right = orderedTimeline.length - 1;
|
|
280
285
|
let best = null;
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
|
|
287
|
+
while (left <= right) {
|
|
288
|
+
const mid = Math.floor((left + right) / 2);
|
|
289
|
+
const entry = orderedTimeline[mid];
|
|
290
|
+
if ((entry?.timestamp || 0) <= target) {
|
|
283
291
|
best = entry;
|
|
284
|
-
|
|
292
|
+
left = mid + 1;
|
|
293
|
+
} else {
|
|
294
|
+
right = mid - 1;
|
|
285
295
|
}
|
|
286
|
-
break;
|
|
287
296
|
}
|
|
288
|
-
|
|
297
|
+
|
|
298
|
+
return best;
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
export function findUsageAtOrBefore(timeline, timestampMs) {
|
|
292
302
|
return findUsageEntryAtOrBefore(timeline, timestampMs)?.usage || null;
|
|
293
303
|
}
|
|
294
304
|
|
|
305
|
+
function getTimestampOrderedTimeline(timeline) {
|
|
306
|
+
if (timeline[SORTED_BY_TIMESTAMP]) return timeline;
|
|
307
|
+
if (isTimestampSorted(timeline)) {
|
|
308
|
+
markTimelineSorted(timeline);
|
|
309
|
+
return timeline;
|
|
310
|
+
}
|
|
311
|
+
const orderedTimeline = [...timeline].sort((left, right) => left.timestamp - right.timestamp);
|
|
312
|
+
markTimelineSorted(orderedTimeline);
|
|
313
|
+
return orderedTimeline;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isTimestampSorted(timeline) {
|
|
317
|
+
return timeline.every((entry, index) => index === 0 || timeline[index - 1].timestamp <= entry.timestamp);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function markTimelineSorted(timeline) {
|
|
321
|
+
Object.defineProperty(timeline, SORTED_BY_TIMESTAMP, {
|
|
322
|
+
value: true,
|
|
323
|
+
configurable: true,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
function shouldUseRipgrep(rolloutPath, minBytes) {
|
|
296
328
|
try {
|
|
297
329
|
return statSync(rolloutPath).size >= minBytes;
|
package/server/sqlite-reader.js
CHANGED
|
@@ -14,14 +14,24 @@ export function readThreads(codexHome, onProgress) {
|
|
|
14
14
|
const count = db.prepare('SELECT count(*) as c FROM threads').get().c;
|
|
15
15
|
if (onProgress) onProgress({ total: count, read: 0 });
|
|
16
16
|
|
|
17
|
+
const hasThreadSpawnEdges = db.prepare(
|
|
18
|
+
'SELECT count(*) as c FROM sqlite_master WHERE type = ? AND name = ?'
|
|
19
|
+
).get('table', 'thread_spawn_edges').c > 0;
|
|
20
|
+
const parentSelect = hasThreadSpawnEdges ? 'e.parent_thread_id' : 'NULL';
|
|
21
|
+
const parentJoin = hasThreadSpawnEdges
|
|
22
|
+
? 'LEFT JOIN thread_spawn_edges e ON e.child_thread_id = t.id'
|
|
23
|
+
: '';
|
|
24
|
+
|
|
17
25
|
const stmt = db.prepare(`
|
|
18
26
|
SELECT
|
|
19
|
-
id, rollout_path, created_at, updated_at,
|
|
20
|
-
source, model_provider, model, reasoning_effort, cwd, title,
|
|
21
|
-
tokens_used, agent_nickname, agent_role,
|
|
22
|
-
cli_version, git_branch, git_origin_url
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
t.id, t.rollout_path, t.created_at, t.updated_at,
|
|
28
|
+
t.source, t.model_provider, t.model, t.reasoning_effort, t.cwd, t.title,
|
|
29
|
+
t.tokens_used, t.agent_nickname, t.agent_role,
|
|
30
|
+
t.cli_version, t.git_branch, t.git_origin_url,
|
|
31
|
+
${parentSelect} AS parent_thread_id
|
|
32
|
+
FROM threads t
|
|
33
|
+
${parentJoin}
|
|
34
|
+
ORDER BY t.created_at ASC
|
|
25
35
|
`);
|
|
26
36
|
|
|
27
37
|
const threads = [];
|
|
@@ -41,6 +51,7 @@ export function readThreads(codexHome, onProgress) {
|
|
|
41
51
|
tokens_used: row.tokens_used || 0,
|
|
42
52
|
agent_nickname: row.agent_nickname || null,
|
|
43
53
|
agent_role: row.agent_role || null,
|
|
54
|
+
parent_thread_id: row.parent_thread_id || null,
|
|
44
55
|
cli_version: row.cli_version || '',
|
|
45
56
|
git_branch: row.git_branch || null,
|
|
46
57
|
});
|