tokentracker-cli 0.88.1 → 0.88.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/dashboard/dist/assets/{AchievementBadge-Crrws1U1.js → AchievementBadge-B6CZtEaS.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C65tiTnu.js → AchievementsPage-YJJREeYN.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Gc28pe4U.js → AchievementsSection-Bz8kzUKE.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D_jEjBVd.js → BadgeDetailModal-B18owwHf.js} +1 -1
- package/dashboard/dist/assets/{Card-DlZueiVg.js → Card-D_HcMRXq.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-DrwVfg1s.js → ClawdAnimated-BR4Vm-wp.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Dj753h1Y.js → CommandPalette-DD0iOGEP.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-PiK4kOib.js → CommunityStatsModal-CfJn-_nP.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CJz-TMiD.js → ConfirmModal-8zQyesYk.js} +1 -1
- package/dashboard/dist/assets/{Controls-DZ0W7HPO.js → Controls-Dd_7ctXC.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DEDZy960.js → DashboardPage-CyIp3wde.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BR60A3uN.js → DevicePage-BjNEbSgO.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-qcbYLZ_r.js → DialogClose-BPRpp4Wp.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-FmibVDMx.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-KMA1u6gh.js → DialogTitle-Cq4jatmU.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BtTL9V_I.js → FadeIn-DoK6rAcp.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Ckp60oxi.js → HeaderGithubStar-B3konO6W.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DaCmGdvh.js → HoverTooltip-BaDkj8r8.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Bs2xgpWT.js → IpCheckPage-BkrAKRYK.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Dj6382lF.js → LandingPage-av_mctyf.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-4LQRczW4.js → LeaderboardAvatar-CgdMLdWa.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BKTbW7CC.js → LeaderboardPage-Vb3i4Qpc.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BP-I9FrE.js → LeaderboardProfileModal-CVxiTLDv.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CWzqXfsO.js → LeaderboardProfilePage-BWIwKo1y.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DJL3rkrZ.js → LimitsPage-CA1Ve4x5.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-CJ8aY8Yg.js → LocalOnlyNotice-Rm3uyEk3.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-RfbqlInv.js → LoginCard-DL0l2lLw.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DEgSV0ho.js → LoginModal-C5Fk3Jms.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-biogQhPJ.js → LoginPage-Cu7DAOnx.js} +1 -1
- package/dashboard/dist/assets/{PetPage-bbmFMllf.js → PetPage-BgqjLomc.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Bx_a2ATx.js → PopoverPopup-CEHZ6QXJ.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-ClMKnRNE.js → ProviderIcon-BC3XWoZ3.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B0aAisWC.js → ResetPasswordPage-BrongFEU.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-85G6K4nz.js → SegmentedControl-D8EHNgNi.js} +1 -1
- package/dashboard/dist/assets/{Select-D-PLRmNY.js → Select-D9k3JTvC.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DOQMVovf.js → SelectItemText-BZ-2piB5.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C2fq26Du.js → ServiceStatusPage-Cv_KU00i.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-B-Gzrsxr.js → SessionsPage-DIPKSBas.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DzjURmD5.js → SettingsPage-ClKxRKyx.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-Bmu5fgPF.js → SkillsPage-DvuwO5Yv.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DFVGDrXI.js → TokenGalaxy-DIM8f80U.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CgWa03di.js → ToolbarRootContext-BprUPTPI.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CbLhCRaf.js → TrendMonitor-DWFiGJ35.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CClBDKNb.js → WidgetsPage-DL-MG-Q7.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B1QPw5JI.js → WrappedPage-CGIDKH61.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CV_fKq4F.js → __vite-browser-external-BKzvjCbJ.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-D-hDhb_B.js → agent-logos-CbNruoos.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DAr-mJ3B.js → arrow-up-right-eLrRdfJt.js} +1 -1
- package/dashboard/dist/assets/{check-DPefl5-t.js → check-DZD0Jp7h.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-MlzWycxC.js → chevron-right-B500Y_Nj.js} +1 -1
- package/dashboard/dist/assets/{copy-B_53rp1c.js → copy-uJNaZG-w.js} +1 -1
- package/dashboard/dist/assets/{download-DnlmdACw.js → download-DzvthQjK.js} +1 -1
- package/dashboard/dist/assets/{external-link-DytBHNpR.js → external-link-CbRIdeZk.js} +1 -1
- package/dashboard/dist/assets/{flame-CO4NpDfZ.js → flame-BGvWQF3z.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CwDQe65M.js → format-tokens-BmLj1DrP.js} +1 -1
- package/dashboard/dist/assets/{icons-DvviMgxO.js → icons-oryPazn7.js} +1 -1
- package/dashboard/dist/assets/{index-B0Lzl2gQ.js → index-BC_awyRs.js} +1 -1
- package/dashboard/dist/assets/{index-Dz9TFADl.js → index-RZ9PpvhZ.js} +1 -1
- package/dashboard/dist/assets/{index-hGGiJmvs.js → index-lCPHhuCN.js} +1 -1
- package/dashboard/dist/assets/{info-BY_SOAny.js → info-DmArnish.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Cpis_64d.js → limits-providers-DmO_jdt4.js} +1 -1
- package/dashboard/dist/assets/{link-2-DaIthsuC.js → link-2-Cp4yrCYD.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DXLU_CS0.js → loader-circle-DGUMo8NS.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CnLZ2V-D.js → maximize-2-BLObxnY1.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CtoD-5nh.js → modifiers.esm-B_-ZYAOP.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-CwfUXtG9.js → monitor-smartphone-CFtFQCf_.js} +1 -1
- package/dashboard/dist/assets/{provider-display-C0gDapSI.js → provider-display-zfwJ-zfN.js} +1 -1
- package/dashboard/dist/assets/{react-BNEThWJE.js → react-C9v6bUwR.js} +1 -1
- package/dashboard/dist/assets/{search-GYLDIV-1.js → search-DcdG91Gl.js} +1 -1
- package/dashboard/dist/assets/{skills-api-noC3sCP_.js → skills-api-Cbj6O4EO.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-Bm4LZh1T.js → sortable.esm-CvVo8fVR.js} +1 -1
- package/dashboard/dist/assets/{store-DWhiUbOS.js → store-BCViEsFt.js} +1 -1
- package/dashboard/dist/assets/{terminal-BEQDw7-4.js → terminal-BBtxMCI7.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Cm7eF7SC.js → trash-2-Dn_E9JQO.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-9a-td9J_.js → use-community-stats-DPAzvqWl.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-XY6qQJai.js → use-limits-display-prefs-DC9B7mbP.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BEaiYX4S.js → use-ordered-list-BUUwYoO2.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-Bs24cR01.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-oDZEyqyO.js → use-session-efficiency-pref-OJfrVVxb.js} +1 -1
- package/dashboard/dist/assets/{use-transform-LKaVzl1V.js → use-transform-CVLI5mjY.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DY3e5o6f.js → use-usage-limits-BXgn5HJr.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CSRY4U-t.js → useCurrency-RNt_kRXK.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-CSZXl0Pl.js → useScrollLock-CGjecig7.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-f1kcdHaP.js → useTokenFormat-CPg1vv3V.js} +1 -1
- package/dashboard/dist/assets/{zap-BMZr9Qc1.js → zap-CamjYQ0R.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/codex-rollout-parser.js +15 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +136 -14
- package/src/lib/session-analytics.js +252 -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",
|
|
@@ -248,21 +250,28 @@ function finalizeRecord(record) {
|
|
|
248
250
|
return record;
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
function readableSessionPaths(filePath) {
|
|
254
|
+
const candidates = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
255
|
+
const readable = candidates.filter((value) => sessionFileStatKey(value));
|
|
256
|
+
if (!readable.length) {
|
|
257
|
+
throw new Error("session files vanished before they could be scanned");
|
|
258
|
+
}
|
|
259
|
+
return readable;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isRecoverableSessionReadError(error) {
|
|
263
|
+
return ["ENOENT", "EACCES", "EPERM", "EISDIR", "EIO", "ESTALE"].includes(error?.code);
|
|
264
|
+
}
|
|
265
|
+
|
|
251
266
|
async function scanClaudeSession(filePath) {
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
// Native and WSL mounts can disappear independently after discovery. Keep
|
|
268
|
+
// the surviving mirror instead of dropping the whole logical session.
|
|
269
|
+
const filePaths = readableSessionPaths(filePath);
|
|
270
|
+
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
254
271
|
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.
|
|
272
|
+
// One logical cross-root group is cached and scanned as a unit, so widening
|
|
273
|
+
// this set across that group stays deterministic. Unrelated files (including
|
|
274
|
+
// same-UUID siblings inside one root) are never grouped here.
|
|
266
275
|
const seenMessages = new Set();
|
|
267
276
|
const bounds = emptyBounds();
|
|
268
277
|
// The basename keeps grouping stable for files that never write a sessionId
|
|
@@ -270,7 +279,7 @@ async function scanClaudeSession(filePath) {
|
|
|
270
279
|
// session_id — otherwise non-session logs under ~/.claude/projects (e.g.
|
|
271
280
|
// skill-injections.jsonl, journal.jsonl) yield `claude --resume journal`,
|
|
272
281
|
// a command that always fails.
|
|
273
|
-
let rawSessionId = path.basename(
|
|
282
|
+
let rawSessionId = path.basename(primaryFilePath, ".jsonl");
|
|
274
283
|
let observedSessionId = null;
|
|
275
284
|
let cwd = null;
|
|
276
285
|
let model = "unknown";
|
|
@@ -290,57 +299,81 @@ async function scanClaudeSession(filePath) {
|
|
|
290
299
|
}
|
|
291
300
|
let lastPromptFingerprint = null;
|
|
292
301
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
302
|
+
// A native path and a WSL path can expose the same logical session with a
|
|
303
|
+
// shared prefix and different tails. Scan every tail, but suppress records
|
|
304
|
+
// already present in an earlier root. Hashes stay in-memory only, preserving
|
|
305
|
+
// the metadata-only persistence contract.
|
|
306
|
+
const priorRecordHashes = new Set();
|
|
307
|
+
let scannedFiles = 0;
|
|
308
|
+
for (const currentFilePath of filePaths) {
|
|
309
|
+
const currentRecordHashes = new Set();
|
|
310
|
+
try {
|
|
311
|
+
const input = fs.createReadStream(currentFilePath, { encoding: "utf8" });
|
|
312
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
313
|
+
for await (const line of lines) {
|
|
314
|
+
if (filePaths.length > 1) {
|
|
315
|
+
const recordHash = crypto.createHash("sha256").update(line).digest("base64url");
|
|
316
|
+
if (priorRecordHashes.has(recordHash)) continue;
|
|
317
|
+
currentRecordHashes.add(recordHash);
|
|
318
|
+
}
|
|
319
|
+
let obj;
|
|
320
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
321
|
+
updateBounds(bounds, obj.timestamp || obj.message?.timestamp);
|
|
322
|
+
if (typeof obj.sessionId === "string" && obj.sessionId) {
|
|
323
|
+
rawSessionId = obj.sessionId;
|
|
324
|
+
observedSessionId = obj.sessionId;
|
|
325
|
+
}
|
|
326
|
+
if (typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
|
|
327
|
+
// Claude writes its own generated one-line summary as an "ai-title" record.
|
|
328
|
+
// It is agent-authored metadata (not the raw prompt body); keep the latest.
|
|
329
|
+
if (obj.type === "ai-title" && typeof obj.aiTitle === "string") {
|
|
330
|
+
const cleaned = cleanSessionTitle(obj.aiTitle);
|
|
331
|
+
if (cleaned) aiTitle = cleaned;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (obj.type === "user") {
|
|
335
|
+
const prompt = extractClaudePrompt(obj);
|
|
336
|
+
if (!prompt) continue;
|
|
337
|
+
const fingerprint = promptFingerprint(prompt);
|
|
338
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
339
|
+
lastPromptFingerprint = fingerprint;
|
|
340
|
+
closeTurn();
|
|
341
|
+
turns += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
345
|
+
const dedupKey = claudeMessageDedupKey(obj);
|
|
346
|
+
if (dedupKey && seenMessages.has(dedupKey)) continue;
|
|
347
|
+
if (dedupKey) seenMessages.add(dedupKey);
|
|
348
|
+
// Claude writes internal summary/observer messages with model
|
|
349
|
+
// "<synthetic>". They are not a billable model and can appear after the
|
|
350
|
+
// real assistant messages in the same session. Keep the latest real
|
|
351
|
+
// model instead of letting that marker overwrite it.
|
|
352
|
+
const candidateModel = normalizeSessionModel(obj.message.model);
|
|
353
|
+
if (candidateModel) model = candidateModel;
|
|
354
|
+
addTotals(tokens, tokenTotals(obj.message.usage));
|
|
355
|
+
const content = Array.isArray(obj.message.content) ? obj.message.content : [];
|
|
356
|
+
for (const block of content) {
|
|
357
|
+
if (!block || block.type !== "tool_use") continue;
|
|
358
|
+
const name = String(block.name || "").toLowerCase();
|
|
359
|
+
if (EDIT_TOOLS.has(name)) currentHadEdit = true;
|
|
360
|
+
if (name === "agent" || name === "task") {
|
|
361
|
+
subagentCalls += 1;
|
|
362
|
+
const subtype = typeof block.input?.subagent_type === "string"
|
|
363
|
+
? block.input.subagent_type.trim().slice(0, 64)
|
|
364
|
+
: "unspecified";
|
|
365
|
+
subagentTypes.set(subtype || "unspecified", (subagentTypes.get(subtype || "unspecified") || 0) + 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
341
368
|
}
|
|
369
|
+
scannedFiles += 1;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (!isRecoverableSessionReadError(error)) throw error;
|
|
372
|
+
} finally {
|
|
373
|
+
for (const recordHash of currentRecordHashes) priorRecordHashes.add(recordHash);
|
|
342
374
|
}
|
|
343
375
|
}
|
|
376
|
+
if (!scannedFiles) throw new Error("all grouped Claude session files failed during read");
|
|
344
377
|
closeTurn();
|
|
345
378
|
return finalizeRecord({
|
|
346
379
|
version: SIDECAR_VERSION,
|
|
@@ -352,7 +385,7 @@ async function scanClaudeSession(filePath) {
|
|
|
352
385
|
// wrote one — the UI then falls back to the project name.
|
|
353
386
|
title: aiTitle,
|
|
354
387
|
source: "claude",
|
|
355
|
-
project_key: projectKey(cwd,
|
|
388
|
+
project_key: projectKey(cwd, primaryFilePath),
|
|
356
389
|
project_ref: cwd || null,
|
|
357
390
|
model,
|
|
358
391
|
...bounds,
|
|
@@ -367,9 +400,8 @@ async function scanClaudeSession(filePath) {
|
|
|
367
400
|
}
|
|
368
401
|
|
|
369
402
|
async function scanCodexDeliverySignals(filePath) {
|
|
403
|
+
const filePaths = readableSessionPaths(filePath);
|
|
370
404
|
const bounds = emptyBounds();
|
|
371
|
-
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
372
|
-
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
373
405
|
let turns = 0;
|
|
374
406
|
let editTurns = 0;
|
|
375
407
|
let retryTurns = 0;
|
|
@@ -394,35 +426,55 @@ async function scanCodexDeliverySignals(filePath) {
|
|
|
394
426
|
turns += 1;
|
|
395
427
|
}
|
|
396
428
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
429
|
+
const priorRecordHashes = new Set();
|
|
430
|
+
let scannedFiles = 0;
|
|
431
|
+
for (const currentFilePath of filePaths) {
|
|
432
|
+
const currentRecordHashes = new Set();
|
|
433
|
+
try {
|
|
434
|
+
const input = fs.createReadStream(currentFilePath, { encoding: "utf8" });
|
|
435
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
436
|
+
for await (const line of lines) {
|
|
437
|
+
if (filePaths.length > 1) {
|
|
438
|
+
const recordHash = crypto.createHash("sha256").update(line).digest("base64url");
|
|
439
|
+
if (priorRecordHashes.has(recordHash)) continue;
|
|
440
|
+
currentRecordHashes.add(recordHash);
|
|
441
|
+
}
|
|
442
|
+
let obj;
|
|
443
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
444
|
+
updateBounds(bounds, obj.timestamp);
|
|
445
|
+
if (obj.type === "turn_context") {
|
|
446
|
+
hasTurnContext = true;
|
|
447
|
+
beginTurn(String(obj.payload?.turn_id || obj.timestamp || turns + 1));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const prompt = extractCodexPrompt(obj);
|
|
451
|
+
if (prompt) {
|
|
452
|
+
const fingerprint = promptFingerprint(prompt);
|
|
453
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
454
|
+
lastPromptFingerprint = fingerprint;
|
|
455
|
+
if (!hasTurnContext) beginTurn(String(obj.timestamp || turns + 1));
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (obj.type !== "response_item") continue;
|
|
459
|
+
const toolNames = extractCodexSignalTools(obj.payload);
|
|
460
|
+
if (!toolNames.length) continue;
|
|
461
|
+
if (!currentTurnOpen) beginTurn(String(obj.timestamp || turns + 1));
|
|
462
|
+
if (toolNames.some((name) => EDIT_TOOLS.has(name))) currentHadEdit = true;
|
|
463
|
+
for (const name of toolNames) {
|
|
464
|
+
if (!CODEX_SUBAGENT_TOOLS.has(name)) continue;
|
|
465
|
+
subagentCalls += 1;
|
|
466
|
+
const displayName = name === "multi_agent_v1__spawn_agent" ? "spawn_agent" : name;
|
|
467
|
+
subagentTypes.set(displayName, (subagentTypes.get(displayName) || 0) + 1);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
scannedFiles += 1;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (!isRecoverableSessionReadError(error)) throw error;
|
|
473
|
+
} finally {
|
|
474
|
+
for (const recordHash of currentRecordHashes) priorRecordHashes.add(recordHash);
|
|
424
475
|
}
|
|
425
476
|
}
|
|
477
|
+
if (!scannedFiles) throw new Error("all grouped Codex signal files failed during read");
|
|
426
478
|
closeTurn();
|
|
427
479
|
return {
|
|
428
480
|
bounds,
|
|
@@ -435,9 +487,11 @@ async function scanCodexDeliverySignals(filePath) {
|
|
|
435
487
|
}
|
|
436
488
|
|
|
437
489
|
async function scanCodexSession(filePath) {
|
|
490
|
+
const filePaths = readableSessionPaths(filePath);
|
|
491
|
+
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
438
492
|
const [parsed, signals] = await Promise.all([
|
|
439
|
-
parseCodexRolloutFile(
|
|
440
|
-
scanCodexDeliverySignals(
|
|
493
|
+
parseCodexRolloutFile(filePaths, { seenTokenEvents: new Set() }),
|
|
494
|
+
scanCodexDeliverySignals(filePaths),
|
|
441
495
|
]);
|
|
442
496
|
const parsedModel = normalizeSessionModel(parsed.model);
|
|
443
497
|
const provider = normalizeSessionModel(parsed.provider);
|
|
@@ -450,16 +504,17 @@ async function scanCodexSession(filePath) {
|
|
|
450
504
|
// Codex's own thread title from session_index.jsonl (Codex-authored
|
|
451
505
|
// metadata, keyed by session id). Null when Codex never named the thread —
|
|
452
506
|
// the UI then falls back to the project name.
|
|
453
|
-
const
|
|
454
|
-
|
|
507
|
+
const title = parsed.sessionId
|
|
508
|
+
? filePaths.map(loadCodexTitleIndex).map((index) => index.get(parsed.sessionId)).find(Boolean) || null
|
|
509
|
+
: null;
|
|
455
510
|
return finalizeRecord({
|
|
456
511
|
version: SIDECAR_VERSION,
|
|
457
|
-
session_hash: sessionHash("codex", parsed.sessionId ||
|
|
512
|
+
session_hash: sessionHash("codex", parsed.sessionId || primaryFilePath),
|
|
458
513
|
session_id: parsed.sessionId || null,
|
|
459
514
|
// Local-only: stripped in summarizeSessions before any cloud/CSV export.
|
|
460
515
|
title,
|
|
461
516
|
source: "codex",
|
|
462
|
-
project_key: projectKey(parsed.cwd,
|
|
517
|
+
project_key: projectKey(parsed.cwd, primaryFilePath),
|
|
463
518
|
project_ref: parsed.cwd || null,
|
|
464
519
|
model,
|
|
465
520
|
...signals.bounds,
|
|
@@ -795,7 +850,10 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
795
850
|
return [...new Set(roots)];
|
|
796
851
|
}
|
|
797
852
|
|
|
798
|
-
//
|
|
853
|
+
// Group one logical session discovered under more than one root. A group is
|
|
854
|
+
// scanned as a unit so identical prefixes count once while divergent tails are
|
|
855
|
+
// both retained. Same-UUID siblings inside one Claude root stay separate: the
|
|
856
|
+
// path alone cannot prove which one a file in another root mirrors.
|
|
799
857
|
//
|
|
800
858
|
// Codex gets this from its session-id pass below; Claude only ever had per-file
|
|
801
859
|
// message dedup (`claudeMessageDedupKey` inside `scanClaudeSession`), which
|
|
@@ -809,13 +867,9 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
809
867
|
// single-install machines are unaffected, and two same-named files inside one
|
|
810
868
|
// tree stay distinct (unproven identity must not delete a session). A basename
|
|
811
869
|
// without a session UUID never participates either.
|
|
812
|
-
function
|
|
870
|
+
function groupClaudeFilesAcrossRoots(groups) {
|
|
813
871
|
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
|
-
};
|
|
872
|
+
if (rootGroups.length <= 1) return [...new Set(rootGroups[0] || [])].map((filePath) => [filePath]);
|
|
819
873
|
const sessionIdOf = (filePath) =>
|
|
820
874
|
path.basename(filePath).match(/^([0-9a-f-]{36})\.jsonl$/i)?.[1] || null;
|
|
821
875
|
|
|
@@ -836,31 +890,85 @@ function dedupeClaudeFilesAcrossRoots(groups) {
|
|
|
836
890
|
}
|
|
837
891
|
}
|
|
838
892
|
|
|
839
|
-
|
|
893
|
+
const groupedBySession = new Map();
|
|
894
|
+
for (const group of rootGroups) {
|
|
895
|
+
for (const filePath of group) {
|
|
896
|
+
const id = sessionIdOf(filePath);
|
|
897
|
+
if (!id || ambiguous.has(id)) continue;
|
|
898
|
+
if (!groupedBySession.has(id)) groupedBySession.set(id, []);
|
|
899
|
+
const paths = groupedBySession.get(id);
|
|
900
|
+
if (!paths.includes(filePath)) paths.push(filePath);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Preserve root order and the first path as the representative cache key.
|
|
840
905
|
const ordered = [];
|
|
841
|
-
const
|
|
906
|
+
const emitted = new Set();
|
|
842
907
|
for (const group of rootGroups) {
|
|
843
908
|
for (const filePath of group) {
|
|
844
909
|
const id = sessionIdOf(filePath);
|
|
845
910
|
if (!id || ambiguous.has(id)) {
|
|
846
|
-
if (!
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (previous === undefined) {
|
|
851
|
-
winnerBySession.set(id, filePath);
|
|
852
|
-
ordered.push(filePath);
|
|
911
|
+
if (!emitted.has(filePath)) {
|
|
912
|
+
ordered.push([filePath]);
|
|
913
|
+
emitted.add(filePath);
|
|
914
|
+
}
|
|
853
915
|
continue;
|
|
854
916
|
}
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
917
|
+
if (emitted.has(id)) continue;
|
|
918
|
+
ordered.push(groupedBySession.get(id) || [filePath]);
|
|
919
|
+
emitted.add(id);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return ordered;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function sameFileContent(filePaths) {
|
|
926
|
+
if (!Array.isArray(filePaths) || filePaths.length <= 1) return true;
|
|
927
|
+
try {
|
|
928
|
+
const first = fs.readFileSync(filePaths[0]);
|
|
929
|
+
return filePaths.slice(1).every((filePath) => {
|
|
930
|
+
const candidate = fs.readFileSync(filePath);
|
|
931
|
+
return candidate.length === first.length && candidate.equals(first);
|
|
932
|
+
});
|
|
933
|
+
} catch {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Compatibility helper used by focused discovery tests and callers that only
|
|
939
|
+
// need a flat list. Only byte-identical mirrors collapse; divergent copies are
|
|
940
|
+
// all returned so no transcript tail is discarded.
|
|
941
|
+
function dedupeClaudeFilesAcrossRoots(groups) {
|
|
942
|
+
const mtimeOf = (filePath) => {
|
|
943
|
+
try { return fs.statSync(filePath).mtimeMs; } catch { return -Infinity; }
|
|
944
|
+
};
|
|
945
|
+
const out = [];
|
|
946
|
+
for (const filePaths of groupClaudeFilesAcrossRoots(groups)) {
|
|
947
|
+
if (filePaths.length <= 1 || !sameFileContent(filePaths)) {
|
|
948
|
+
out.push(...filePaths);
|
|
949
|
+
continue;
|
|
861
950
|
}
|
|
951
|
+
out.push(filePaths.reduce((winner, candidate) => (
|
|
952
|
+
mtimeOf(candidate) > mtimeOf(winner) ? candidate : winner
|
|
953
|
+
)));
|
|
862
954
|
}
|
|
863
|
-
return [...new Set(
|
|
955
|
+
return [...new Set(out)];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function groupCodexFiles(filePaths) {
|
|
959
|
+
const ordered = [];
|
|
960
|
+
const bySession = new Map();
|
|
961
|
+
for (const filePath of [...new Set(filePaths || [])]) {
|
|
962
|
+
const id = path.basename(filePath).match(/([0-9a-f-]{36})\.jsonl$/i)?.[1] || filePath;
|
|
963
|
+
let group = bySession.get(id);
|
|
964
|
+
if (!group) {
|
|
965
|
+
group = [];
|
|
966
|
+
bySession.set(id, group);
|
|
967
|
+
ordered.push(group);
|
|
968
|
+
}
|
|
969
|
+
group.push(filePath);
|
|
970
|
+
}
|
|
971
|
+
return ordered;
|
|
864
972
|
}
|
|
865
973
|
|
|
866
974
|
async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
@@ -873,26 +981,17 @@ async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
|
873
981
|
Promise.all(codexRoots.map((r) => listRolloutFilesDeep(path.join(r, "archived_sessions")))),
|
|
874
982
|
listGrokSessionFiles(path.join(grokHome, "sessions")),
|
|
875
983
|
]);
|
|
876
|
-
const allClaude =
|
|
984
|
+
const allClaude = groupClaudeFilesAcrossRoots(claudeGroups);
|
|
877
985
|
const codex = [...new Set(codexGroups.flat())];
|
|
878
986
|
const archived = [...new Set(archivedGroups.flat())];
|
|
879
987
|
// Claude Memory stores thousands of background observer transcripts beside
|
|
880
988
|
// real Claude Code sessions. They contain <synthetic>/haiku bookkeeping and
|
|
881
989
|
// no user coding outcome, so scanning them both slows the card dramatically
|
|
882
990
|
// and dilutes its efficiency metrics.
|
|
883
|
-
const claude = allClaude.filter((
|
|
991
|
+
const claude = allClaude.filter((filePaths) => !filePaths.some((filePath) => filePath
|
|
884
992
|
.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 };
|
|
993
|
+
.some((segment) => segment.endsWith(CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX))));
|
|
994
|
+
return { claude, codex: groupCodexFiles([...codex, ...archived]), grok };
|
|
896
995
|
}
|
|
897
996
|
|
|
898
997
|
function filesSignature(files) {
|
|
@@ -907,9 +1006,10 @@ function filesSignature(files) {
|
|
|
907
1006
|
}
|
|
908
1007
|
|
|
909
1008
|
function sessionFileCacheKey(source, filePath) {
|
|
1009
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
910
1010
|
return crypto
|
|
911
1011
|
.createHash("sha256")
|
|
912
|
-
.update(`${source}\0${path.resolve(
|
|
1012
|
+
.update(`${source}\0${filePaths.map((value) => path.resolve(value)).join("\0")}`)
|
|
913
1013
|
.digest("hex")
|
|
914
1014
|
.slice(0, 24);
|
|
915
1015
|
}
|
|
@@ -964,13 +1064,19 @@ function loadCodexTitleIndex(filePath) {
|
|
|
964
1064
|
// Grok titles live in sibling summary.json (and signals can change without
|
|
965
1065
|
// touching updates.jsonl on some partial flushes).
|
|
966
1066
|
function analyticsEntryStatKey(source, filePath) {
|
|
967
|
-
const
|
|
968
|
-
|
|
1067
|
+
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
1068
|
+
const sessionStats = filePaths.map((value) => sessionFileStatKey(value) || "missing");
|
|
1069
|
+
if (sessionStats.every((value) => value === "missing")) return null;
|
|
1070
|
+
const sessionStat = sessionStats.join("|");
|
|
969
1071
|
if (source === "codex") {
|
|
970
|
-
|
|
1072
|
+
const titleStats = filePaths.map((value) => (
|
|
1073
|
+
sessionFileStatKey(codexTitleIndexPathFor(value)) || "missing"
|
|
1074
|
+
));
|
|
1075
|
+
return `${sessionStat}|title-index:${titleStats.join("|")}`;
|
|
971
1076
|
}
|
|
972
1077
|
if (source === "grok") {
|
|
973
|
-
|
|
1078
|
+
const primary = filePaths[0];
|
|
1079
|
+
return `${sessionStat}|summary:${sessionFileStatKey(grokSummaryPathFor(primary)) || "missing"}|signals:${sessionFileStatKey(grokSignalsPathFor(primary)) || "missing"}`;
|
|
974
1080
|
}
|
|
975
1081
|
return sessionStat;
|
|
976
1082
|
}
|
|
@@ -1011,9 +1117,9 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1011
1117
|
// per-file dependency check below on the next refresh. Grok titles/metadata
|
|
1012
1118
|
// live in sibling summary.json / signals.json next to updates.jsonl.
|
|
1013
1119
|
const signature = filesSignature([
|
|
1014
|
-
...discovered.claude,
|
|
1015
|
-
...discovered.codex,
|
|
1016
|
-
...discovered.codex.map(codexTitleIndexPathFor).filter(Boolean),
|
|
1120
|
+
...discovered.claude.flat(),
|
|
1121
|
+
...discovered.codex.flat(),
|
|
1122
|
+
...discovered.codex.flat().map(codexTitleIndexPathFor).filter(Boolean),
|
|
1017
1123
|
...discovered.grok,
|
|
1018
1124
|
...discovered.grok.map(grokSummaryPathFor),
|
|
1019
1125
|
...discovered.grok.map(grokSignalsPathFor),
|
|
@@ -1035,8 +1141,8 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1035
1141
|
const nextFiles = {};
|
|
1036
1142
|
const sessions = [];
|
|
1037
1143
|
const entries = [
|
|
1038
|
-
...discovered.claude.map((
|
|
1039
|
-
...discovered.codex.map((
|
|
1144
|
+
...discovered.claude.map((filePaths) => ({ source: "claude", filePath: filePaths, scan: scanClaudeSession })),
|
|
1145
|
+
...discovered.codex.map((filePaths) => ({ source: "codex", filePath: filePaths, scan: scanCodexSession })),
|
|
1040
1146
|
...discovered.grok.map((filePath) => ({ source: "grok", filePath, scan: scanGrokSession })),
|
|
1041
1147
|
];
|
|
1042
1148
|
// Files we could not turn into a row (permission denied, half-written line,
|
|
@@ -1371,4 +1477,5 @@ module.exports = {
|
|
|
1371
1477
|
sessionsToCsv,
|
|
1372
1478
|
providerRoots,
|
|
1373
1479
|
dedupeClaudeFilesAcrossRoots,
|
|
1480
|
+
analyticsEntryStatKey,
|
|
1374
1481
|
};
|
|
@@ -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};
|