tokentracker-cli 0.88.1 → 0.88.2
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/dashboard/dist/assets/{AchievementBadge-Crrws1U1.js → AchievementBadge-BcCrg832.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C65tiTnu.js → AchievementsPage-C95dF_C-.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Gc28pe4U.js → AchievementsSection-B-VD8CfN.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D_jEjBVd.js → BadgeDetailModal-CDTJ3tR6.js} +1 -1
- package/dashboard/dist/assets/{Card-DlZueiVg.js → Card-a0RpvhLn.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-DrwVfg1s.js → ClawdAnimated-CaLKUbmC.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Dj753h1Y.js → CommandPalette-CQpkX7dK.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-PiK4kOib.js → CommunityStatsModal-Cj53g1ky.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CJz-TMiD.js → ConfirmModal-FHZmD91i.js} +1 -1
- package/dashboard/dist/assets/{Controls-DZ0W7HPO.js → Controls-BSk2Eph4.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DEDZy960.js → DashboardPage-Ci1jVw-F.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BR60A3uN.js → DevicePage-BnCWVmOI.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-qcbYLZ_r.js → DialogClose-BUhPBSeF.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Dp7bBdfJ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-KMA1u6gh.js → DialogTitle-CbNKnmTY.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BtTL9V_I.js → FadeIn-D87p8xfM.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Ckp60oxi.js → HeaderGithubStar-kE4I1PyE.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DaCmGdvh.js → HoverTooltip-miw72aLh.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Bs2xgpWT.js → IpCheckPage-D-JoHTz-.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Dj6382lF.js → LandingPage-Dx_MOVjJ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-4LQRczW4.js → LeaderboardAvatar-arnp_cWI.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BKTbW7CC.js → LeaderboardPage-zsoFUX5G.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BP-I9FrE.js → LeaderboardProfileModal-CrmSTl9n.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CWzqXfsO.js → LeaderboardProfilePage-BSE0fPco.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DJL3rkrZ.js → LimitsPage-BrU9e8AV.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-CJ8aY8Yg.js → LocalOnlyNotice-BWDZKn00.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-RfbqlInv.js → LoginCard-DS8mxxs_.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DEgSV0ho.js → LoginModal-DhINfVDV.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-biogQhPJ.js → LoginPage-BKmuhR74.js} +1 -1
- package/dashboard/dist/assets/{PetPage-bbmFMllf.js → PetPage-QQk_B1SJ.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Bx_a2ATx.js → PopoverPopup-DS6ml7IE.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-ClMKnRNE.js → ProviderIcon-DkHp1Hrl.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B0aAisWC.js → ResetPasswordPage-CvJiufPF.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-85G6K4nz.js → SegmentedControl-DbYmpdj3.js} +1 -1
- package/dashboard/dist/assets/{Select-D-PLRmNY.js → Select-DJcVfYm7.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DOQMVovf.js → SelectItemText-DUQLkHGQ.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C2fq26Du.js → ServiceStatusPage-DqPQUM1f.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-B-Gzrsxr.js → SessionsPage-CPOSombb.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DzjURmD5.js → SettingsPage-nIfi1h7D.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-Bmu5fgPF.js → SkillsPage-BnngQwIu.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DFVGDrXI.js → TokenGalaxy-CAVmlIYW.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CgWa03di.js → ToolbarRootContext-CyG7xQo2.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CbLhCRaf.js → TrendMonitor-DyuKVbzV.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CClBDKNb.js → WidgetsPage-72n9-HCD.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B1QPw5JI.js → WrappedPage-Dp1aUOyA.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CV_fKq4F.js → __vite-browser-external-8kU62_i_.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-D-hDhb_B.js → agent-logos-ChfjzWPV.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DAr-mJ3B.js → arrow-up-right-CiTMPVa_.js} +1 -1
- package/dashboard/dist/assets/{check-DPefl5-t.js → check-OQW1qgf_.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-MlzWycxC.js → chevron-right-BitImfyV.js} +1 -1
- package/dashboard/dist/assets/{copy-B_53rp1c.js → copy-BM76c1Ob.js} +1 -1
- package/dashboard/dist/assets/{download-DnlmdACw.js → download-DaKmqoH2.js} +1 -1
- package/dashboard/dist/assets/{external-link-DytBHNpR.js → external-link-AL639M-l.js} +1 -1
- package/dashboard/dist/assets/{flame-CO4NpDfZ.js → flame-BeYD0c6y.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CwDQe65M.js → format-tokens-DK1Fdxfq.js} +1 -1
- package/dashboard/dist/assets/{icons-DvviMgxO.js → icons-BtLj_BDi.js} +1 -1
- package/dashboard/dist/assets/{index-Dz9TFADl.js → index-BHBegQjp.js} +1 -1
- package/dashboard/dist/assets/{index-B0Lzl2gQ.js → index-BK2Bm5Vp.js} +1 -1
- package/dashboard/dist/assets/{index-hGGiJmvs.js → index-D4kdhgAz.js} +1 -1
- package/dashboard/dist/assets/{info-BY_SOAny.js → info-CIwDFvrE.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Cpis_64d.js → limits-providers-BFkGzLM4.js} +1 -1
- package/dashboard/dist/assets/{link-2-DaIthsuC.js → link-2-xFS-GP5p.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DXLU_CS0.js → loader-circle-83n_EWic.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CnLZ2V-D.js → maximize-2-CevrJZKM.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CtoD-5nh.js → modifiers.esm-a1oXNZlE.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-CwfUXtG9.js → monitor-smartphone-CSP2FeVP.js} +1 -1
- package/dashboard/dist/assets/{provider-display-C0gDapSI.js → provider-display-CamXa0pj.js} +1 -1
- package/dashboard/dist/assets/{react-BNEThWJE.js → react-DV1nb3lW.js} +1 -1
- package/dashboard/dist/assets/{search-GYLDIV-1.js → search-wj8Kxgr5.js} +1 -1
- package/dashboard/dist/assets/{skills-api-noC3sCP_.js → skills-api-BNpGEfgh.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-Bm4LZh1T.js → sortable.esm-Xh3nfzFR.js} +1 -1
- package/dashboard/dist/assets/{store-DWhiUbOS.js → store-lBXh9-l6.js} +1 -1
- package/dashboard/dist/assets/{terminal-BEQDw7-4.js → terminal-uNBlGBuA.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Cm7eF7SC.js → trash-2-08YOSvJh.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-9a-td9J_.js → use-community-stats-CMyCuzcM.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-XY6qQJai.js → use-limits-display-prefs-BkqvWQ4X.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BEaiYX4S.js → use-ordered-list-CJ9F2mdG.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CD_DKLbl.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-oDZEyqyO.js → use-session-efficiency-pref-Hf4gvuAl.js} +1 -1
- package/dashboard/dist/assets/{use-transform-LKaVzl1V.js → use-transform-BBP_cds4.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DY3e5o6f.js → use-usage-limits-DsLD0OEm.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CSRY4U-t.js → useCurrency-CXngtOdH.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-CSZXl0Pl.js → useScrollLock-Q65zPe5d.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-f1kcdHaP.js → useTokenFormat-CE5zbI_h.js} +1 -1
- package/dashboard/dist/assets/{zap-BMZr9Qc1.js → zap-CHYAc2Be.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +4 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +136 -14
- package/src/lib/session-analytics.js +220 -145
- package/dashboard/dist/assets/DialogDescription-DMUlPgjH.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C9_Q8rAl.js +0 -1
|
@@ -52,7 +52,9 @@ const wsl = require("./wsl-probe");
|
|
|
52
52
|
// v10 adds Grok Build sessions (~/.grok/sessions/**/updates.jsonl), scanned
|
|
53
53
|
// from turn_completed.usage + tool_call metadata. Bump so Claude/Codex rows
|
|
54
54
|
// stay valid while Grok entries appear on the next full rebuild.
|
|
55
|
-
|
|
55
|
+
// v11 groups native/WSL copies of one logical Claude/Codex session and scans
|
|
56
|
+
// their union, deduping shared records while retaining divergent tails.
|
|
57
|
+
const SIDECAR_VERSION = 11;
|
|
56
58
|
const EDIT_TOOLS = new Set([
|
|
57
59
|
"apply_patch",
|
|
58
60
|
"edit",
|
|
@@ -249,20 +251,12 @@ function finalizeRecord(record) {
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
async function scanClaudeSession(filePath) {
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
255
|
+
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
254
256
|
const tokens = emptyTotals();
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// a cross-file set would make a row's tokens depend on which other files were
|
|
259
|
-
// scanned first — and the incremental cache reuses rows without replaying
|
|
260
|
-
// that state. Measured cost of the per-file scope on real data: 808 assistant
|
|
261
|
-
// messages appear in more than one file (fork/resume replays history into a
|
|
262
|
-
// new session file), inflating per-session tokens by ~1.5% in aggregate.
|
|
263
|
-
// Fixing it needs a deterministic owner (e.g. earliest started_at) plus
|
|
264
|
-
// message keys persisted per row — a sidecar redesign, not a one-liner. Do
|
|
265
|
-
// NOT "fix" this by widening the set without solving the cache interaction.
|
|
257
|
+
// One logical cross-root group is cached and scanned as a unit, so widening
|
|
258
|
+
// this set across that group stays deterministic. Unrelated files (including
|
|
259
|
+
// same-UUID siblings inside one root) are never grouped here.
|
|
266
260
|
const seenMessages = new Set();
|
|
267
261
|
const bounds = emptyBounds();
|
|
268
262
|
// The basename keeps grouping stable for files that never write a sessionId
|
|
@@ -270,7 +264,7 @@ async function scanClaudeSession(filePath) {
|
|
|
270
264
|
// session_id — otherwise non-session logs under ~/.claude/projects (e.g.
|
|
271
265
|
// skill-injections.jsonl, journal.jsonl) yield `claude --resume journal`,
|
|
272
266
|
// a command that always fails.
|
|
273
|
-
let rawSessionId = path.basename(
|
|
267
|
+
let rawSessionId = path.basename(primaryFilePath, ".jsonl");
|
|
274
268
|
let observedSessionId = null;
|
|
275
269
|
let cwd = null;
|
|
276
270
|
let model = "unknown";
|
|
@@ -290,56 +284,72 @@ async function scanClaudeSession(filePath) {
|
|
|
290
284
|
}
|
|
291
285
|
let lastPromptFingerprint = null;
|
|
292
286
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
287
|
+
// A native path and a WSL path can expose the same logical session with a
|
|
288
|
+
// shared prefix and different tails. Scan every tail, but suppress records
|
|
289
|
+
// already present in an earlier root. Hashes stay in-memory only, preserving
|
|
290
|
+
// the metadata-only persistence contract.
|
|
291
|
+
const priorRecordHashes = new Set();
|
|
292
|
+
for (const currentFilePath of filePaths) {
|
|
293
|
+
const currentRecordHashes = new Set();
|
|
294
|
+
const input = fs.createReadStream(currentFilePath, { encoding: "utf8" });
|
|
295
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
296
|
+
for await (const line of lines) {
|
|
297
|
+
if (filePaths.length > 1) {
|
|
298
|
+
const recordHash = crypto.createHash("sha256").update(line).digest("base64url");
|
|
299
|
+
if (priorRecordHashes.has(recordHash)) continue;
|
|
300
|
+
currentRecordHashes.add(recordHash);
|
|
301
|
+
}
|
|
302
|
+
let obj;
|
|
303
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
304
|
+
updateBounds(bounds, obj.timestamp || obj.message?.timestamp);
|
|
305
|
+
if (typeof obj.sessionId === "string" && obj.sessionId) {
|
|
306
|
+
rawSessionId = obj.sessionId;
|
|
307
|
+
observedSessionId = obj.sessionId;
|
|
308
|
+
}
|
|
309
|
+
if (typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
|
|
310
|
+
// Claude writes its own generated one-line summary as an "ai-title" record.
|
|
311
|
+
// It is agent-authored metadata (not the raw prompt body); keep the latest.
|
|
312
|
+
if (obj.type === "ai-title" && typeof obj.aiTitle === "string") {
|
|
313
|
+
const cleaned = cleanSessionTitle(obj.aiTitle);
|
|
314
|
+
if (cleaned) aiTitle = cleaned;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (obj.type === "user") {
|
|
318
|
+
const prompt = extractClaudePrompt(obj);
|
|
319
|
+
if (!prompt) continue;
|
|
320
|
+
const fingerprint = promptFingerprint(prompt);
|
|
321
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
322
|
+
lastPromptFingerprint = fingerprint;
|
|
323
|
+
closeTurn();
|
|
324
|
+
turns += 1;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
328
|
+
const dedupKey = claudeMessageDedupKey(obj);
|
|
329
|
+
if (dedupKey && seenMessages.has(dedupKey)) continue;
|
|
330
|
+
if (dedupKey) seenMessages.add(dedupKey);
|
|
331
|
+
// Claude writes internal summary/observer messages with model
|
|
332
|
+
// "<synthetic>". They are not a billable model and can appear after the
|
|
333
|
+
// real assistant messages in the same session. Keep the latest real
|
|
334
|
+
// model instead of letting that marker overwrite it.
|
|
335
|
+
const candidateModel = normalizeSessionModel(obj.message.model);
|
|
336
|
+
if (candidateModel) model = candidateModel;
|
|
337
|
+
addTotals(tokens, tokenTotals(obj.message.usage));
|
|
338
|
+
const content = Array.isArray(obj.message.content) ? obj.message.content : [];
|
|
339
|
+
for (const block of content) {
|
|
340
|
+
if (!block || block.type !== "tool_use") continue;
|
|
341
|
+
const name = String(block.name || "").toLowerCase();
|
|
342
|
+
if (EDIT_TOOLS.has(name)) currentHadEdit = true;
|
|
343
|
+
if (name === "agent" || name === "task") {
|
|
344
|
+
subagentCalls += 1;
|
|
345
|
+
const subtype = typeof block.input?.subagent_type === "string"
|
|
346
|
+
? block.input.subagent_type.trim().slice(0, 64)
|
|
347
|
+
: "unspecified";
|
|
348
|
+
subagentTypes.set(subtype || "unspecified", (subagentTypes.get(subtype || "unspecified") || 0) + 1);
|
|
349
|
+
}
|
|
341
350
|
}
|
|
342
351
|
}
|
|
352
|
+
for (const recordHash of currentRecordHashes) priorRecordHashes.add(recordHash);
|
|
343
353
|
}
|
|
344
354
|
closeTurn();
|
|
345
355
|
return finalizeRecord({
|
|
@@ -352,7 +362,7 @@ async function scanClaudeSession(filePath) {
|
|
|
352
362
|
// wrote one — the UI then falls back to the project name.
|
|
353
363
|
title: aiTitle,
|
|
354
364
|
source: "claude",
|
|
355
|
-
project_key: projectKey(cwd,
|
|
365
|
+
project_key: projectKey(cwd, primaryFilePath),
|
|
356
366
|
project_ref: cwd || null,
|
|
357
367
|
model,
|
|
358
368
|
...bounds,
|
|
@@ -367,9 +377,8 @@ async function scanClaudeSession(filePath) {
|
|
|
367
377
|
}
|
|
368
378
|
|
|
369
379
|
async function scanCodexDeliverySignals(filePath) {
|
|
380
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
370
381
|
const bounds = emptyBounds();
|
|
371
|
-
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
372
|
-
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
373
382
|
let turns = 0;
|
|
374
383
|
let editTurns = 0;
|
|
375
384
|
let retryTurns = 0;
|
|
@@ -394,34 +403,46 @@ async function scanCodexDeliverySignals(filePath) {
|
|
|
394
403
|
turns += 1;
|
|
395
404
|
}
|
|
396
405
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
406
|
+
const priorRecordHashes = new Set();
|
|
407
|
+
for (const currentFilePath of filePaths) {
|
|
408
|
+
const currentRecordHashes = new Set();
|
|
409
|
+
const input = fs.createReadStream(currentFilePath, { encoding: "utf8" });
|
|
410
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
411
|
+
for await (const line of lines) {
|
|
412
|
+
if (filePaths.length > 1) {
|
|
413
|
+
const recordHash = crypto.createHash("sha256").update(line).digest("base64url");
|
|
414
|
+
if (priorRecordHashes.has(recordHash)) continue;
|
|
415
|
+
currentRecordHashes.add(recordHash);
|
|
416
|
+
}
|
|
417
|
+
let obj;
|
|
418
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
419
|
+
updateBounds(bounds, obj.timestamp);
|
|
420
|
+
if (obj.type === "turn_context") {
|
|
421
|
+
hasTurnContext = true;
|
|
422
|
+
beginTurn(String(obj.payload?.turn_id || obj.timestamp || turns + 1));
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const prompt = extractCodexPrompt(obj);
|
|
426
|
+
if (prompt) {
|
|
427
|
+
const fingerprint = promptFingerprint(prompt);
|
|
428
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
429
|
+
lastPromptFingerprint = fingerprint;
|
|
430
|
+
if (!hasTurnContext) beginTurn(String(obj.timestamp || turns + 1));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (obj.type !== "response_item") continue;
|
|
434
|
+
const toolNames = extractCodexSignalTools(obj.payload);
|
|
435
|
+
if (!toolNames.length) continue;
|
|
436
|
+
if (!currentTurnOpen) beginTurn(String(obj.timestamp || turns + 1));
|
|
437
|
+
if (toolNames.some((name) => EDIT_TOOLS.has(name))) currentHadEdit = true;
|
|
438
|
+
for (const name of toolNames) {
|
|
439
|
+
if (!CODEX_SUBAGENT_TOOLS.has(name)) continue;
|
|
440
|
+
subagentCalls += 1;
|
|
441
|
+
const displayName = name === "multi_agent_v1__spawn_agent" ? "spawn_agent" : name;
|
|
442
|
+
subagentTypes.set(displayName, (subagentTypes.get(displayName) || 0) + 1);
|
|
443
|
+
}
|
|
424
444
|
}
|
|
445
|
+
for (const recordHash of currentRecordHashes) priorRecordHashes.add(recordHash);
|
|
425
446
|
}
|
|
426
447
|
closeTurn();
|
|
427
448
|
return {
|
|
@@ -435,9 +456,11 @@ async function scanCodexDeliverySignals(filePath) {
|
|
|
435
456
|
}
|
|
436
457
|
|
|
437
458
|
async function scanCodexSession(filePath) {
|
|
459
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
460
|
+
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
438
461
|
const [parsed, signals] = await Promise.all([
|
|
439
|
-
parseCodexRolloutFile(
|
|
440
|
-
scanCodexDeliverySignals(
|
|
462
|
+
parseCodexRolloutFile(filePaths, { seenTokenEvents: new Set() }),
|
|
463
|
+
scanCodexDeliverySignals(filePaths),
|
|
441
464
|
]);
|
|
442
465
|
const parsedModel = normalizeSessionModel(parsed.model);
|
|
443
466
|
const provider = normalizeSessionModel(parsed.provider);
|
|
@@ -450,16 +473,17 @@ async function scanCodexSession(filePath) {
|
|
|
450
473
|
// Codex's own thread title from session_index.jsonl (Codex-authored
|
|
451
474
|
// metadata, keyed by session id). Null when Codex never named the thread —
|
|
452
475
|
// the UI then falls back to the project name.
|
|
453
|
-
const
|
|
454
|
-
|
|
476
|
+
const title = parsed.sessionId
|
|
477
|
+
? filePaths.map(loadCodexTitleIndex).map((index) => index.get(parsed.sessionId)).find(Boolean) || null
|
|
478
|
+
: null;
|
|
455
479
|
return finalizeRecord({
|
|
456
480
|
version: SIDECAR_VERSION,
|
|
457
|
-
session_hash: sessionHash("codex", parsed.sessionId ||
|
|
481
|
+
session_hash: sessionHash("codex", parsed.sessionId || primaryFilePath),
|
|
458
482
|
session_id: parsed.sessionId || null,
|
|
459
483
|
// Local-only: stripped in summarizeSessions before any cloud/CSV export.
|
|
460
484
|
title,
|
|
461
485
|
source: "codex",
|
|
462
|
-
project_key: projectKey(parsed.cwd,
|
|
486
|
+
project_key: projectKey(parsed.cwd, primaryFilePath),
|
|
463
487
|
project_ref: parsed.cwd || null,
|
|
464
488
|
model,
|
|
465
489
|
...signals.bounds,
|
|
@@ -795,7 +819,10 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
795
819
|
return [...new Set(roots)];
|
|
796
820
|
}
|
|
797
821
|
|
|
798
|
-
//
|
|
822
|
+
// Group one logical session discovered under more than one root. A group is
|
|
823
|
+
// scanned as a unit so identical prefixes count once while divergent tails are
|
|
824
|
+
// both retained. Same-UUID siblings inside one Claude root stay separate: the
|
|
825
|
+
// path alone cannot prove which one a file in another root mirrors.
|
|
799
826
|
//
|
|
800
827
|
// Codex gets this from its session-id pass below; Claude only ever had per-file
|
|
801
828
|
// message dedup (`claudeMessageDedupKey` inside `scanClaudeSession`), which
|
|
@@ -809,13 +836,9 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
809
836
|
// single-install machines are unaffected, and two same-named files inside one
|
|
810
837
|
// tree stay distinct (unproven identity must not delete a session). A basename
|
|
811
838
|
// without a session UUID never participates either.
|
|
812
|
-
function
|
|
839
|
+
function groupClaudeFilesAcrossRoots(groups) {
|
|
813
840
|
const rootGroups = (groups || []).filter((group) => Array.isArray(group) && group.length > 0);
|
|
814
|
-
if (rootGroups.length <= 1) return [...new Set(rootGroups[0] || [])];
|
|
815
|
-
|
|
816
|
-
const mtimeOf = (filePath) => {
|
|
817
|
-
try { return fs.statSync(filePath).mtimeMs; } catch { return -Infinity; }
|
|
818
|
-
};
|
|
841
|
+
if (rootGroups.length <= 1) return [...new Set(rootGroups[0] || [])].map((filePath) => [filePath]);
|
|
819
842
|
const sessionIdOf = (filePath) =>
|
|
820
843
|
path.basename(filePath).match(/^([0-9a-f-]{36})\.jsonl$/i)?.[1] || null;
|
|
821
844
|
|
|
@@ -836,31 +859,85 @@ function dedupeClaudeFilesAcrossRoots(groups) {
|
|
|
836
859
|
}
|
|
837
860
|
}
|
|
838
861
|
|
|
839
|
-
|
|
862
|
+
const groupedBySession = new Map();
|
|
863
|
+
for (const group of rootGroups) {
|
|
864
|
+
for (const filePath of group) {
|
|
865
|
+
const id = sessionIdOf(filePath);
|
|
866
|
+
if (!id || ambiguous.has(id)) continue;
|
|
867
|
+
if (!groupedBySession.has(id)) groupedBySession.set(id, []);
|
|
868
|
+
const paths = groupedBySession.get(id);
|
|
869
|
+
if (!paths.includes(filePath)) paths.push(filePath);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Preserve root order and the first path as the representative cache key.
|
|
840
874
|
const ordered = [];
|
|
841
|
-
const
|
|
875
|
+
const emitted = new Set();
|
|
842
876
|
for (const group of rootGroups) {
|
|
843
877
|
for (const filePath of group) {
|
|
844
878
|
const id = sessionIdOf(filePath);
|
|
845
879
|
if (!id || ambiguous.has(id)) {
|
|
846
|
-
if (!
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (previous === undefined) {
|
|
851
|
-
winnerBySession.set(id, filePath);
|
|
852
|
-
ordered.push(filePath);
|
|
880
|
+
if (!emitted.has(filePath)) {
|
|
881
|
+
ordered.push([filePath]);
|
|
882
|
+
emitted.add(filePath);
|
|
883
|
+
}
|
|
853
884
|
continue;
|
|
854
885
|
}
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
winnerBySession.set(id, filePath);
|
|
859
|
-
ordered[ordered.indexOf(previous)] = filePath;
|
|
860
|
-
}
|
|
886
|
+
if (emitted.has(id)) continue;
|
|
887
|
+
ordered.push(groupedBySession.get(id) || [filePath]);
|
|
888
|
+
emitted.add(id);
|
|
861
889
|
}
|
|
862
890
|
}
|
|
863
|
-
return
|
|
891
|
+
return ordered;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function sameFileContent(filePaths) {
|
|
895
|
+
if (!Array.isArray(filePaths) || filePaths.length <= 1) return true;
|
|
896
|
+
try {
|
|
897
|
+
const first = fs.readFileSync(filePaths[0]);
|
|
898
|
+
return filePaths.slice(1).every((filePath) => {
|
|
899
|
+
const candidate = fs.readFileSync(filePath);
|
|
900
|
+
return candidate.length === first.length && candidate.equals(first);
|
|
901
|
+
});
|
|
902
|
+
} catch {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Compatibility helper used by focused discovery tests and callers that only
|
|
908
|
+
// need a flat list. Only byte-identical mirrors collapse; divergent copies are
|
|
909
|
+
// all returned so no transcript tail is discarded.
|
|
910
|
+
function dedupeClaudeFilesAcrossRoots(groups) {
|
|
911
|
+
const mtimeOf = (filePath) => {
|
|
912
|
+
try { return fs.statSync(filePath).mtimeMs; } catch { return -Infinity; }
|
|
913
|
+
};
|
|
914
|
+
const out = [];
|
|
915
|
+
for (const filePaths of groupClaudeFilesAcrossRoots(groups)) {
|
|
916
|
+
if (filePaths.length <= 1 || !sameFileContent(filePaths)) {
|
|
917
|
+
out.push(...filePaths);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
out.push(filePaths.reduce((winner, candidate) => (
|
|
921
|
+
mtimeOf(candidate) > mtimeOf(winner) ? candidate : winner
|
|
922
|
+
)));
|
|
923
|
+
}
|
|
924
|
+
return [...new Set(out)];
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function groupCodexFiles(filePaths) {
|
|
928
|
+
const ordered = [];
|
|
929
|
+
const bySession = new Map();
|
|
930
|
+
for (const filePath of [...new Set(filePaths || [])]) {
|
|
931
|
+
const id = path.basename(filePath).match(/([0-9a-f-]{36})\.jsonl$/i)?.[1] || filePath;
|
|
932
|
+
let group = bySession.get(id);
|
|
933
|
+
if (!group) {
|
|
934
|
+
group = [];
|
|
935
|
+
bySession.set(id, group);
|
|
936
|
+
ordered.push(group);
|
|
937
|
+
}
|
|
938
|
+
group.push(filePath);
|
|
939
|
+
}
|
|
940
|
+
return ordered;
|
|
864
941
|
}
|
|
865
942
|
|
|
866
943
|
async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
@@ -873,26 +950,17 @@ async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
|
873
950
|
Promise.all(codexRoots.map((r) => listRolloutFilesDeep(path.join(r, "archived_sessions")))),
|
|
874
951
|
listGrokSessionFiles(path.join(grokHome, "sessions")),
|
|
875
952
|
]);
|
|
876
|
-
const allClaude =
|
|
953
|
+
const allClaude = groupClaudeFilesAcrossRoots(claudeGroups);
|
|
877
954
|
const codex = [...new Set(codexGroups.flat())];
|
|
878
955
|
const archived = [...new Set(archivedGroups.flat())];
|
|
879
956
|
// Claude Memory stores thousands of background observer transcripts beside
|
|
880
957
|
// real Claude Code sessions. They contain <synthetic>/haiku bookkeeping and
|
|
881
958
|
// no user coding outcome, so scanning them both slows the card dramatically
|
|
882
959
|
// and dilutes its efficiency metrics.
|
|
883
|
-
const claude = allClaude.filter((
|
|
960
|
+
const claude = allClaude.filter((filePaths) => !filePaths.some((filePath) => filePath
|
|
884
961
|
.split(path.sep)
|
|
885
|
-
.some((segment) => segment.endsWith(CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX)));
|
|
886
|
-
|
|
887
|
-
const mtimeOf = (filePath) => {
|
|
888
|
-
try { return fs.statSync(filePath).mtimeMs; } catch { return -Infinity; }
|
|
889
|
-
};
|
|
890
|
-
for (const filePath of [...codex, ...archived]) {
|
|
891
|
-
const id = path.basename(filePath).match(/([0-9a-f-]{36})\.jsonl$/i)?.[1] || filePath;
|
|
892
|
-
const previous = codexBySession.get(id);
|
|
893
|
-
if (!previous || mtimeOf(filePath) > mtimeOf(previous)) codexBySession.set(id, filePath);
|
|
894
|
-
}
|
|
895
|
-
return { claude, codex: [...codexBySession.values()], grok };
|
|
962
|
+
.some((segment) => segment.endsWith(CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX))));
|
|
963
|
+
return { claude, codex: groupCodexFiles([...codex, ...archived]), grok };
|
|
896
964
|
}
|
|
897
965
|
|
|
898
966
|
function filesSignature(files) {
|
|
@@ -907,9 +975,10 @@ function filesSignature(files) {
|
|
|
907
975
|
}
|
|
908
976
|
|
|
909
977
|
function sessionFileCacheKey(source, filePath) {
|
|
978
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
910
979
|
return crypto
|
|
911
980
|
.createHash("sha256")
|
|
912
|
-
.update(`${source}\0${path.resolve(
|
|
981
|
+
.update(`${source}\0${filePaths.map((value) => path.resolve(value)).join("\0")}`)
|
|
913
982
|
.digest("hex")
|
|
914
983
|
.slice(0, 24);
|
|
915
984
|
}
|
|
@@ -964,13 +1033,19 @@ function loadCodexTitleIndex(filePath) {
|
|
|
964
1033
|
// Grok titles live in sibling summary.json (and signals can change without
|
|
965
1034
|
// touching updates.jsonl on some partial flushes).
|
|
966
1035
|
function analyticsEntryStatKey(source, filePath) {
|
|
967
|
-
const
|
|
968
|
-
|
|
1036
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
1037
|
+
const sessionStats = filePaths.map((value) => sessionFileStatKey(value));
|
|
1038
|
+
if (sessionStats.some((value) => !value)) return null;
|
|
1039
|
+
const sessionStat = sessionStats.join("|");
|
|
969
1040
|
if (source === "codex") {
|
|
970
|
-
|
|
1041
|
+
const titleStats = filePaths.map((value) => (
|
|
1042
|
+
sessionFileStatKey(codexTitleIndexPathFor(value)) || "missing"
|
|
1043
|
+
));
|
|
1044
|
+
return `${sessionStat}|title-index:${titleStats.join("|")}`;
|
|
971
1045
|
}
|
|
972
1046
|
if (source === "grok") {
|
|
973
|
-
|
|
1047
|
+
const primary = filePaths[0];
|
|
1048
|
+
return `${sessionStat}|summary:${sessionFileStatKey(grokSummaryPathFor(primary)) || "missing"}|signals:${sessionFileStatKey(grokSignalsPathFor(primary)) || "missing"}`;
|
|
974
1049
|
}
|
|
975
1050
|
return sessionStat;
|
|
976
1051
|
}
|
|
@@ -1011,9 +1086,9 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1011
1086
|
// per-file dependency check below on the next refresh. Grok titles/metadata
|
|
1012
1087
|
// live in sibling summary.json / signals.json next to updates.jsonl.
|
|
1013
1088
|
const signature = filesSignature([
|
|
1014
|
-
...discovered.claude,
|
|
1015
|
-
...discovered.codex,
|
|
1016
|
-
...discovered.codex.map(codexTitleIndexPathFor).filter(Boolean),
|
|
1089
|
+
...discovered.claude.flat(),
|
|
1090
|
+
...discovered.codex.flat(),
|
|
1091
|
+
...discovered.codex.flat().map(codexTitleIndexPathFor).filter(Boolean),
|
|
1017
1092
|
...discovered.grok,
|
|
1018
1093
|
...discovered.grok.map(grokSummaryPathFor),
|
|
1019
1094
|
...discovered.grok.map(grokSignalsPathFor),
|
|
@@ -1035,8 +1110,8 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1035
1110
|
const nextFiles = {};
|
|
1036
1111
|
const sessions = [];
|
|
1037
1112
|
const entries = [
|
|
1038
|
-
...discovered.claude.map((
|
|
1039
|
-
...discovered.codex.map((
|
|
1113
|
+
...discovered.claude.map((filePaths) => ({ source: "claude", filePath: filePaths, scan: scanClaudeSession })),
|
|
1114
|
+
...discovered.codex.map((filePaths) => ({ source: "codex", filePath: filePaths, scan: scanCodexSession })),
|
|
1040
1115
|
...discovered.grok.map((filePath) => ({ source: "grok", filePath, scan: scanGrokSession })),
|
|
1041
1116
|
];
|
|
1042
1117
|
// Files we could not turn into a row (permission denied, half-written line,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,z as a}from"./__vite-browser-external-CV_fKq4F.js";import{u as p}from"./DialogTitle-KMA1u6gh.js";import{u as c}from"./useScrollLock-CSZXl0Pl.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as o}from"./__vite-browser-external-CV_fKq4F.js";import{h as r,i as t,p as s}from"./react-BNEThWJE.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};
|