tokentracker-cli 0.87.8 → 0.87.10
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-CpxX02OK.js → AchievementBadge-BG9bVXuQ.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CSms8IjI.js → AchievementsPage-DwYIajgz.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Sm43tnkh.js → AchievementsSection-BPuhWCWC.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-cSNqMLsK.js → BadgeDetailModal-BU81AHIH.js} +1 -1
- package/dashboard/dist/assets/{Card-wNwNRVSb.js → Card-CxiGV6bE.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-S8sIS2Ca.js → ClawdAnimated-NHcv2_KK.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DVeiwbPM.js → CommandPalette-BKmTa5b8.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-g5P4j3um.js → CommunityStatsModal-nhHtSw7U.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-2P2D5iIp.js → ConfirmModal-DCicMVqg.js} +1 -1
- package/dashboard/dist/assets/{Controls-CCaotoQP.js → Controls-CtjjJ8gt.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DxPGTJpe.js → DashboardPage-BL-NfLIM.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DuIiuci7.js +1 -0
- package/dashboard/dist/assets/{DialogClose-D0giFwO3.js → DialogClose-8GK6rJCf.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C9StmaUp.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-JS_04eUC.js → DialogTitle-EBphaJIt.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-tFzTWDkC.js → FadeIn-B8099njp.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B2EUJUTH.js → HeaderGithubStar-Cyxc4vKa.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BJzHksPT.js → HoverTooltip-BcXNDc20.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B8mA0mut.js → IpCheckPage-Bn5aOiJK.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DmJZDKr9.js → LandingPage-BMbP2M_2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-CDeRQUCg.js → LeaderboardAvatar-sRNGsVJE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Dkgm89kY.js → LeaderboardPage-DUv9hCNQ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-B6_kuR1z.js → LeaderboardProfileModal-D66DzEVL.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BRs8sJBV.js → LeaderboardProfilePage-CWzNEHjJ.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DZ1s5NWV.js → LimitsPage-B7dTsxXt.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cu_hSnux.js → LocalOnlyNotice-BCR-4Ipk.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-yDZbGsHR.js → LoginCard-Dt7v1Pgi.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DEvzCUfQ.js → LoginModal-BaKS0AvW.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DBySF9do.js → LoginPage-J7ir_N3K.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DnzH9t-r.js → PetPage-uAxjE9Vs.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DgFsEMax.js → PopoverPopup-WRGNwhEe.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BEixe-xP.js → ProviderIcon-BTZXNlML.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-excKqsCL.js → ResetPasswordPage-B_TvqHvF.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DDFkH64o.js → SegmentedControl-C1NPnzuC.js} +1 -1
- package/dashboard/dist/assets/{Select-ChEPIuJf.js → Select-DetlANRp.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText--b7ISnJm.js → SelectItemText-CdhCuD7u.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DtAY-Qnn.js → ServiceStatusPage-yMTV03js.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CKFglvuj.js → SessionsPage-rJw-lY6d.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-s0605qWg.js → SettingsPage-BHIGCXa8.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DTsbCucq.js → SkillsPage-BZTTHrKR.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-C_M7q8S4.js → TokenGalaxy-D2tFBs42.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CXQwpea-.js → ToolbarRootContext-CD4Dh9IV.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-1Yhlplf0.js → TrendMonitor-mL7Z3Pu9.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DC6pmtld.js → WidgetsPage-Dech66hD.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DixoqR13.js → WrappedPage-CYaLijy2.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-5aLUeBkM.js → __vite-browser-external-CcMKDoRA.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-DILTbPLg.js → agent-logos-BzFhwsm5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DgIDgjVW.js → arrow-up-right-TrcWBi5O.js} +1 -1
- package/dashboard/dist/assets/{check-Bz6l09VH.js → check-CKWLtHpO.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-EvGQXbI3.js → chevron-right-BjL_B3tf.js} +1 -1
- package/dashboard/dist/assets/{copy-DbsB0OVy.js → copy-B5XqmEDV.js} +1 -1
- package/dashboard/dist/assets/{download-R8nUvXYh.js → download-BtVO_3_3.js} +1 -1
- package/dashboard/dist/assets/{external-link-x0oNzGeq.js → external-link-CTpQdeXU.js} +1 -1
- package/dashboard/dist/assets/{flame-Bo4IdG6k.js → flame-CMixyq1C.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BSGC1-Gz.js → format-tokens-DOgiRUv2.js} +1 -1
- package/dashboard/dist/assets/{icons-D0Okncmo.js → icons-DNpysD5S.js} +1 -1
- package/dashboard/dist/assets/{index-D6F8eNr_.js → index-B7Zuq72U.js} +1 -1
- package/dashboard/dist/assets/{index-Bvvmei-I.js → index-Dadn8f0V.js} +1 -1
- package/dashboard/dist/assets/{index-BbOkC6wf.js → index-k5haeuyB.js} +1 -1
- package/dashboard/dist/assets/{info-DXXKNpLG.js → info-CodBJXwL.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DYa4fdsE.js → limits-providers-DiSK8fij.js} +1 -1
- package/dashboard/dist/assets/{link-2-C9mxEnT6.js → link-2-BE84-D1K.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Y8V8Mq9L.js → loader-circle-62hGDJej.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-QuP69Xcg.js → maximize-2-Dn0j7N4g.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B3SleRKs.js → modifiers.esm-D48EfMXn.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-B2LslYi7.js → monitor-smartphone-B4O-1qqp.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DzBwrDZp.js → provider-display-BBlUUXou.js} +1 -1
- package/dashboard/dist/assets/{react-BooKG84W.js → react-B7hdp-KU.js} +1 -1
- package/dashboard/dist/assets/{search-zdYuHmL-.js → search-DYcfh3D9.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C2IkKfwE.js → skills-api-BL0Jj4TS.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BHvl5vG5.js → sortable.esm-UJyxEfLh.js} +1 -1
- package/dashboard/dist/assets/{store-BBP4jx0R.js → store-koEJUHLx.js} +1 -1
- package/dashboard/dist/assets/{terminal-C0nhFNQ0.js → terminal-CA8uz-Rp.js} +1 -1
- package/dashboard/dist/assets/{trash-2-DNVSqbxA.js → trash-2-Cwdcw5RS.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BK070d1s.js → use-community-stats-BIBrzbia.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BXOUxblO.js → use-limits-display-prefs-BCdQk1ck.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-0SVHZHaW.js → use-ordered-list-Bf9Ozum9.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DGTFk58t.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-eaUpwuLK.js → use-session-efficiency-pref-C83fbwfz.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Bhar2U4w.js → use-transform-DnR7Iz0p.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BGGCMVzi.js → use-usage-limits-D_MtlDKv.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-gIJVmq3w.js → useCurrency-DVO1i6vp.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-BhFzeRtR.js → useScrollLock-CW2lrPuY.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BZ8WT-A6.js → useTokenFormat-BFhS3XRy.js} +1 -1
- package/dashboard/dist/assets/{zap-Dvc_Jt1t.js → zap-DCY5z8oI.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 +430 -1
- package/src/lib/install-resolver.js +22 -6
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +488 -60
- package/dashboard/dist/assets/DevicePage-_nLwNash.js +0 -1
- package/dashboard/dist/assets/DialogDescription-8veCvaUb.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-abZbtoVS.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -83,6 +83,7 @@ const {
|
|
|
83
83
|
listAntigravityTranscripts,
|
|
84
84
|
parseAntigravityIncremental,
|
|
85
85
|
resolveCodebuddyProjectFiles,
|
|
86
|
+
codebuddyJsonlHasUsage,
|
|
86
87
|
parseCodebuddyIncremental,
|
|
87
88
|
resolveWorkbuddyProjectFiles,
|
|
88
89
|
parseWorkbuddyIncremental,
|
|
@@ -142,6 +143,8 @@ const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
|
|
|
142
143
|
const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
|
|
143
144
|
const CLAUDE_MEM_OBSERVER_REINCLUDE_KEY = "claudeMemObserverReinclude_2026_05_v3";
|
|
144
145
|
const GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY = "grokAppendOnlyRepair_2026_05_v4";
|
|
146
|
+
const CODEBUDDY_LOG_JSONL_REPAIR_KEY = "codebuddyLogJsonlOverlapRepair_2026_08";
|
|
147
|
+
const WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY = "workbuddyContextUsageRepair_2026_08";
|
|
145
148
|
const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
|
|
146
149
|
// v1 had a cursor-format bug (wrote plain integer instead of {inode, offset,
|
|
147
150
|
// updatedAt}), which made parseClaudeIncremental reread every jsonl from
|
|
@@ -589,7 +592,16 @@ async function cmdSync(argv, context = {}) {
|
|
|
589
592
|
const wslCodexDir = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
|
|
590
593
|
? wsl.discoverWslHome(".codex")
|
|
591
594
|
: null;
|
|
592
|
-
|
|
595
|
+
// resolveInstallPaths stays the single authority for wsl-first /
|
|
596
|
+
// native-first / wsl-only / native-only / both selection; requireAnyChild
|
|
597
|
+
// makes it validate that a candidate actually holds sessions/ or
|
|
598
|
+
// archived_sessions/ so an empty WSL ~/.codex shell cannot shadow the
|
|
599
|
+
// native install (issue #codex-wsl-shadow).
|
|
600
|
+
const codexPaths = resolveInstallPaths({
|
|
601
|
+
nativeValue: codexNativeValue,
|
|
602
|
+
wslValue: wslCodexDir,
|
|
603
|
+
requireAnyChild: ["sessions", "archived_sessions"],
|
|
604
|
+
});
|
|
593
605
|
if (codexPaths.native) {
|
|
594
606
|
sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
|
|
595
607
|
if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
|
|
@@ -1770,6 +1782,26 @@ async function cmdSync(argv, context = {}) {
|
|
|
1770
1782
|
// Tencent's CodeBuddy CLI is a Claude Code clone; no hook system, so we
|
|
1771
1783
|
// tail the per-session JSONL conversation logs incrementally on each sync.
|
|
1772
1784
|
let codebuddyResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1785
|
+
if (isFullSourceScan && sourceAllowed("codebuddy")) {
|
|
1786
|
+
const repairCodebuddyFiles = mergeBothFileSources({
|
|
1787
|
+
resolveFiles: (env) => resolveCodebuddyProjectFiles({
|
|
1788
|
+
...env,
|
|
1789
|
+
TOKENTRACKER_CODEBUDDY_LOG_FALLBACK: "1",
|
|
1790
|
+
}),
|
|
1791
|
+
env: process.env,
|
|
1792
|
+
});
|
|
1793
|
+
try {
|
|
1794
|
+
await repairCodebuddyLogJsonlOverlap({
|
|
1795
|
+
cursors,
|
|
1796
|
+
queuePath,
|
|
1797
|
+
queueStatePath,
|
|
1798
|
+
codebuddyFiles: repairCodebuddyFiles,
|
|
1799
|
+
env: process.env,
|
|
1800
|
+
});
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
warnProviderParseFailure("CodeBuddy log/JSONL repair", err, opts);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1773
1805
|
const codebuddyFiles = sourceAllowed("codebuddy")
|
|
1774
1806
|
? mergeBothFileSources({ resolveFiles: resolveCodebuddyProjectFiles, env: process.env })
|
|
1775
1807
|
: [];
|
|
@@ -1806,6 +1838,19 @@ async function cmdSync(argv, context = {}) {
|
|
|
1806
1838
|
? mergeBothFileSources({ resolveFiles: resolveWorkbuddyProjectFiles, env: process.env })
|
|
1807
1839
|
: [];
|
|
1808
1840
|
if (sourceAllowed("workbuddy")) {
|
|
1841
|
+
if (isFullSourceScan) {
|
|
1842
|
+
try {
|
|
1843
|
+
await repairWorkbuddyContextUsage({
|
|
1844
|
+
cursors,
|
|
1845
|
+
queuePath,
|
|
1846
|
+
queueStatePath,
|
|
1847
|
+
workbuddyFiles,
|
|
1848
|
+
env: process.env,
|
|
1849
|
+
});
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
warnProviderParseFailure("WorkBuddy context-usage repair", err, opts);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1809
1854
|
if (progress?.enabled) {
|
|
1810
1855
|
progress.start(`Parsing WorkBuddy ${renderBar(0)} | buckets 0`);
|
|
1811
1856
|
}
|
|
@@ -2730,6 +2775,9 @@ module.exports = {
|
|
|
2730
2775
|
acquireSyncLock,
|
|
2731
2776
|
migrateCursorUnknownBuckets,
|
|
2732
2777
|
migrateRolloutCumulativeDeltaBuckets,
|
|
2778
|
+
repairCodebuddyLogJsonlOverlap,
|
|
2779
|
+
repairWorkbuddyContextUsage,
|
|
2780
|
+
WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY,
|
|
2733
2781
|
repairCodexRescanInflation,
|
|
2734
2782
|
repairCodexForkReplayInflation,
|
|
2735
2783
|
repairCodexInterleavedUsageInflation,
|
|
@@ -2744,6 +2792,7 @@ module.exports = {
|
|
|
2744
2792
|
recordCodexColdScanAudit,
|
|
2745
2793
|
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
2746
2794
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
2795
|
+
CODEBUDDY_LOG_JSONL_REPAIR_KEY,
|
|
2747
2796
|
CODEX_RESCAN_DEDUP_REPAIR_KEY,
|
|
2748
2797
|
CODEX_FORK_REPLAY_REPAIR_KEY,
|
|
2749
2798
|
CODEX_USAGE_LINEAGE_REPAIR_KEY,
|
|
@@ -3809,6 +3858,386 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
|
|
|
3809
3858
|
cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
|
|
3810
3859
|
}
|
|
3811
3860
|
|
|
3861
|
+
function codebuddyUsageRowKey(row) {
|
|
3862
|
+
if (!row || row.source !== "codebuddy") return null;
|
|
3863
|
+
const model = typeof row.model === "string" && row.model ? row.model : "unknown";
|
|
3864
|
+
const hourStart = typeof row.hour_start === "string" && row.hour_start ? row.hour_start : null;
|
|
3865
|
+
return hourStart ? bucketKey("codebuddy", model, hourStart) : null;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
function codebuddyZeroUsageRow(model, hourStart) {
|
|
3869
|
+
return {
|
|
3870
|
+
source: "codebuddy",
|
|
3871
|
+
model: model || "unknown",
|
|
3872
|
+
hour_start: hourStart,
|
|
3873
|
+
input_tokens: 0,
|
|
3874
|
+
cached_input_tokens: 0,
|
|
3875
|
+
cache_creation_input_tokens: 0,
|
|
3876
|
+
output_tokens: 0,
|
|
3877
|
+
reasoning_output_tokens: 0,
|
|
3878
|
+
total_tokens: 0,
|
|
3879
|
+
billable_total_tokens: 0,
|
|
3880
|
+
conversation_count: 0,
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
// Issue #403 historical repair. Before the JSONL usage fix, CodeBuddy could
|
|
3885
|
+
// ingest the same round-trip once from ~/.codebuddy/projects and once from the
|
|
3886
|
+
// IDE extension log. The forward resolver now disables logs whenever any JSONL
|
|
3887
|
+
// has usable rawUsage, but existing queues/hourly state need one guarded,
|
|
3888
|
+
// atomic rebuild so the old double-count is not left visible forever.
|
|
3889
|
+
async function repairCodebuddyLogJsonlOverlap({
|
|
3890
|
+
cursors,
|
|
3891
|
+
queuePath,
|
|
3892
|
+
queueStatePath,
|
|
3893
|
+
codebuddyFiles,
|
|
3894
|
+
env = process.env,
|
|
3895
|
+
} = {}) {
|
|
3896
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
3897
|
+
const migrations = (cursors.migrations ||= {});
|
|
3898
|
+
const prior = migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY];
|
|
3899
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
3900
|
+
|
|
3901
|
+
const files = Array.isArray(codebuddyFiles) ? codebuddyFiles : [];
|
|
3902
|
+
const jsonlFiles = files.filter((filePath) => typeof filePath === "string" && filePath.endsWith(".jsonl"));
|
|
3903
|
+
const logFiles = files.filter((filePath) => typeof filePath === "string" && filePath.endsWith(".log"));
|
|
3904
|
+
const hasDetailedJsonl = jsonlFiles.some((filePath) => codebuddyJsonlHasUsage(filePath));
|
|
3905
|
+
|
|
3906
|
+
const hourly = cursors.hourly && typeof cursors.hourly === "object" ? cursors.hourly : {};
|
|
3907
|
+
const liveHourlyKeys = new Set(
|
|
3908
|
+
Object.keys(hourly.buckets || {}).filter((key) => key.startsWith("codebuddy|")),
|
|
3909
|
+
);
|
|
3910
|
+
let queueRaw = "";
|
|
3911
|
+
try {
|
|
3912
|
+
queueRaw = await fs.readFile(queuePath, "utf8");
|
|
3913
|
+
} catch (e) {
|
|
3914
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3915
|
+
}
|
|
3916
|
+
const queueCodebuddyKeys = new Set();
|
|
3917
|
+
const keptQueueLines = [];
|
|
3918
|
+
for (const line of queueRaw.split("\n")) {
|
|
3919
|
+
if (!line.trim()) continue;
|
|
3920
|
+
let row;
|
|
3921
|
+
try {
|
|
3922
|
+
row = JSON.parse(line);
|
|
3923
|
+
} catch (_e) {
|
|
3924
|
+
keptQueueLines.push(line);
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
const key = codebuddyUsageRowKey(row);
|
|
3928
|
+
if (!key) {
|
|
3929
|
+
keptQueueLines.push(line);
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3932
|
+
queueCodebuddyKeys.add(key);
|
|
3933
|
+
}
|
|
3934
|
+
const liveKeys = new Set([...liveHourlyKeys, ...queueCodebuddyKeys]);
|
|
3935
|
+
if (liveKeys.size === 0) {
|
|
3936
|
+
migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
|
|
3937
|
+
status: "noop",
|
|
3938
|
+
appliedAt: new Date().toISOString(),
|
|
3939
|
+
reason: "no-codebuddy-history",
|
|
3940
|
+
};
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
// Keep this condition retryable when old CodeBuddy history exists but the
|
|
3945
|
+
// extension log is temporarily rotated/missing. A permanent no-op here
|
|
3946
|
+
// would close the repair window before a later full scan can rediscover it.
|
|
3947
|
+
if (jsonlFiles.length === 0 || logFiles.length === 0 || !hasDetailedJsonl) {
|
|
3948
|
+
migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
|
|
3949
|
+
skipped: true,
|
|
3950
|
+
reason: "no-mixed-detailed-sources",
|
|
3951
|
+
at: new Date().toISOString(),
|
|
3952
|
+
};
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// A rebuild from the currently discoverable JSONL is safe only if every
|
|
3957
|
+
// file previously tailed by CodeBuddy is still available in this scan. If a
|
|
3958
|
+
// rotated/deleted log is missing, retain the user's history and retry later
|
|
3959
|
+
// rather than silently erasing an unreproducible bucket.
|
|
3960
|
+
const discovered = new Set(files);
|
|
3961
|
+
const priorFiles = cursors.codebuddy?.fileOffsets && typeof cursors.codebuddy.fileOffsets === "object"
|
|
3962
|
+
? Object.keys(cursors.codebuddy.fileOffsets)
|
|
3963
|
+
: [];
|
|
3964
|
+
const missingPriorFile = priorFiles.find((filePath) => !discovered.has(filePath));
|
|
3965
|
+
if (missingPriorFile) {
|
|
3966
|
+
migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
|
|
3967
|
+
skipped: true,
|
|
3968
|
+
reason: "codebuddy_file_unreproducible",
|
|
3969
|
+
filePath: missingPriorFile,
|
|
3970
|
+
at: new Date().toISOString(),
|
|
3971
|
+
};
|
|
3972
|
+
return false;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
const tmpQueue = `${queuePath}.codebuddyrebuild.${process.pid}.${Date.now()}`;
|
|
3976
|
+
const tmpCursors = {
|
|
3977
|
+
version: 1,
|
|
3978
|
+
hourly: { buckets: {}, groupQueued: {} },
|
|
3979
|
+
codebuddy: {},
|
|
3980
|
+
};
|
|
3981
|
+
let rebuiltRows = [];
|
|
3982
|
+
try {
|
|
3983
|
+
await parseCodebuddyIncremental({
|
|
3984
|
+
// Rebuild from both sources. The parser now preserves legacy-only log
|
|
3985
|
+
// sessions while consuming mirrored log fingerprints against JSONL, so
|
|
3986
|
+
// rebuilding JSONL alone would silently discard valid pre-rawUsage data.
|
|
3987
|
+
projectFiles: files,
|
|
3988
|
+
cursors: tmpCursors,
|
|
3989
|
+
queuePath: tmpQueue,
|
|
3990
|
+
env,
|
|
3991
|
+
});
|
|
3992
|
+
let rebuiltRaw = "";
|
|
3993
|
+
try {
|
|
3994
|
+
rebuiltRaw = await fs.readFile(tmpQueue, "utf8");
|
|
3995
|
+
} catch (e) {
|
|
3996
|
+
if (e?.code !== "ENOENT") throw e;
|
|
3997
|
+
}
|
|
3998
|
+
rebuiltRows = rebuiltRaw.split("\n").filter((line) => line.trim());
|
|
3999
|
+
} catch (e) {
|
|
4000
|
+
console.error("[sync] CodeBuddy log/JSONL repair: rebuild failed, leaving history untouched:", e?.message || e);
|
|
4001
|
+
return false;
|
|
4002
|
+
} finally {
|
|
4003
|
+
await fs.rm(tmpQueue, { force: true }).catch(() => {});
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
const rebuiltKeys = new Set();
|
|
4007
|
+
const validRebuiltRows = [];
|
|
4008
|
+
for (const line of rebuiltRows) {
|
|
4009
|
+
let row;
|
|
4010
|
+
try { row = JSON.parse(line); } catch (_e) { continue; }
|
|
4011
|
+
const key = codebuddyUsageRowKey(row);
|
|
4012
|
+
if (!key) continue;
|
|
4013
|
+
rebuiltKeys.add(key);
|
|
4014
|
+
validRebuiltRows.push(line);
|
|
4015
|
+
}
|
|
4016
|
+
if (rebuiltKeys.size === 0) {
|
|
4017
|
+
migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
|
|
4018
|
+
skipped: true,
|
|
4019
|
+
reason: "jsonl_rebuild_empty",
|
|
4020
|
+
at: new Date().toISOString(),
|
|
4021
|
+
};
|
|
4022
|
+
return false;
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
const staleRetractions = [];
|
|
4026
|
+
for (const key of liveKeys) {
|
|
4027
|
+
if (rebuiltKeys.has(key)) continue;
|
|
4028
|
+
const [, model, ...hourParts] = key.split("|");
|
|
4029
|
+
staleRetractions.push(JSON.stringify(codebuddyZeroUsageRow(model, hourParts.join("|"))));
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
const nextQueueLines = keptQueueLines.concat(staleRetractions, validRebuiltRows);
|
|
4033
|
+
await ensureDir(path.dirname(queuePath));
|
|
4034
|
+
const queueTmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
|
|
4035
|
+
await fs.writeFile(queueTmp, nextQueueLines.length ? `${nextQueueLines.join("\n")}\n` : "", "utf8");
|
|
4036
|
+
await fs.rename(queueTmp, queuePath);
|
|
4037
|
+
|
|
4038
|
+
const nextHourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
|
|
4039
|
+
nextHourly.buckets ||= {};
|
|
4040
|
+
nextHourly.groupQueued ||= {};
|
|
4041
|
+
for (const key of Object.keys(nextHourly.buckets)) {
|
|
4042
|
+
if (key.startsWith("codebuddy|")) delete nextHourly.buckets[key];
|
|
4043
|
+
}
|
|
4044
|
+
for (const key of Object.keys(nextHourly.groupQueued)) {
|
|
4045
|
+
if (key.startsWith("codebuddy|")) delete nextHourly.groupQueued[key];
|
|
4046
|
+
}
|
|
4047
|
+
for (const [key, bucket] of Object.entries(tmpCursors.hourly.buckets || {})) {
|
|
4048
|
+
if (key.startsWith("codebuddy|")) nextHourly.buckets[key] = bucket;
|
|
4049
|
+
}
|
|
4050
|
+
for (const [key, value] of Object.entries(tmpCursors.hourly.groupQueued || {})) {
|
|
4051
|
+
if (key.startsWith("codebuddy|")) nextHourly.groupQueued[key] = value;
|
|
4052
|
+
}
|
|
4053
|
+
cursors.codebuddy = tmpCursors.codebuddy || {};
|
|
4054
|
+
|
|
4055
|
+
let uploadState = {};
|
|
4056
|
+
try { uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8")); } catch (_e) {}
|
|
4057
|
+
uploadState.offset = 0;
|
|
4058
|
+
uploadState.updatedAt = new Date().toISOString();
|
|
4059
|
+
uploadState.note = "reset_after_codebuddy_log_jsonl_repair_2026_08";
|
|
4060
|
+
await ensureDir(path.dirname(queueStatePath));
|
|
4061
|
+
await fs.writeFile(queueStatePath, JSON.stringify(uploadState, null, 2) + "\n", "utf8");
|
|
4062
|
+
|
|
4063
|
+
migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
|
|
4064
|
+
status: "applied",
|
|
4065
|
+
appliedAt: new Date().toISOString(),
|
|
4066
|
+
jsonlFiles: jsonlFiles.length,
|
|
4067
|
+
logFiles: logFiles.length,
|
|
4068
|
+
previousBuckets: liveKeys.size,
|
|
4069
|
+
rebuiltBuckets: rebuiltKeys.size,
|
|
4070
|
+
retractedBuckets: staleRetractions.length,
|
|
4071
|
+
};
|
|
4072
|
+
return true;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// WorkBuddy's 0.87.9 SQLite fallback treated the bounded session_usage.used
|
|
4076
|
+
// context snapshot as cumulative input tokens. A current database proves that
|
|
4077
|
+
// assumption false (`size` is a 192k context limit while trace input totals can
|
|
4078
|
+
// exceed it), so rebuild historical WorkBuddy buckets from JSONL/trace sources
|
|
4079
|
+
// once the new parser is available. The same missing-source guard used by the
|
|
4080
|
+
// other data migrations keeps this recoverable: if a previously contributing
|
|
4081
|
+
// file is gone, leave history untouched and stop adding new context snapshots.
|
|
4082
|
+
async function repairWorkbuddyContextUsage({
|
|
4083
|
+
cursors,
|
|
4084
|
+
queuePath,
|
|
4085
|
+
queueStatePath,
|
|
4086
|
+
workbuddyFiles,
|
|
4087
|
+
env = process.env,
|
|
4088
|
+
} = {}) {
|
|
4089
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
4090
|
+
const migrations = (cursors.migrations ||= {});
|
|
4091
|
+
const prior = migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY];
|
|
4092
|
+
if (prior && !(typeof prior === "object" && prior.skipped)) return false;
|
|
4093
|
+
|
|
4094
|
+
const hourly = cursors.hourly && typeof cursors.hourly === "object" ? cursors.hourly : {};
|
|
4095
|
+
const liveHourlyKeys = new Set(
|
|
4096
|
+
Object.keys(hourly.buckets || {}).filter((key) => key.startsWith("workbuddy|")),
|
|
4097
|
+
);
|
|
4098
|
+
let queueRaw = "";
|
|
4099
|
+
try {
|
|
4100
|
+
queueRaw = await fs.readFile(queuePath, "utf8");
|
|
4101
|
+
} catch (e) {
|
|
4102
|
+
if (e?.code !== "ENOENT") throw e;
|
|
4103
|
+
}
|
|
4104
|
+
const queueWorkbuddyKeys = new Set();
|
|
4105
|
+
const keptQueueLines = [];
|
|
4106
|
+
for (const line of queueRaw.split("\n")) {
|
|
4107
|
+
if (!line.trim()) continue;
|
|
4108
|
+
let row;
|
|
4109
|
+
try { row = JSON.parse(line); } catch (_e) {
|
|
4110
|
+
keptQueueLines.push(line);
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
if (row && row.source === "workbuddy" && typeof row.model === "string" && typeof row.hour_start === "string") {
|
|
4114
|
+
queueWorkbuddyKeys.add(bucketKey("workbuddy", row.model || "unknown", row.hour_start));
|
|
4115
|
+
} else {
|
|
4116
|
+
keptQueueLines.push(line);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
const liveKeys = new Set([...liveHourlyKeys, ...queueWorkbuddyKeys]);
|
|
4120
|
+
if (liveKeys.size === 0) {
|
|
4121
|
+
migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
|
|
4122
|
+
status: "noop",
|
|
4123
|
+
appliedAt: new Date().toISOString(),
|
|
4124
|
+
reason: "no-workbuddy-history",
|
|
4125
|
+
};
|
|
4126
|
+
return false;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
const files = Array.isArray(workbuddyFiles) ? workbuddyFiles : [];
|
|
4130
|
+
const discoveredPaths = new Set(
|
|
4131
|
+
files.map((entry) => typeof entry === "string" ? entry : entry?.path).filter(Boolean),
|
|
4132
|
+
);
|
|
4133
|
+
const priorFiles = cursors.workbuddy?.fileOffsets && typeof cursors.workbuddy.fileOffsets === "object"
|
|
4134
|
+
? Object.keys(cursors.workbuddy.fileOffsets)
|
|
4135
|
+
: [];
|
|
4136
|
+
if (files.length === 0 || priorFiles.some((filePath) => !discoveredPaths.has(filePath))) {
|
|
4137
|
+
migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
|
|
4138
|
+
skipped: true,
|
|
4139
|
+
reason: files.length === 0 ? "no-rebuild-sources" : "workbuddy_file_unreproducible",
|
|
4140
|
+
at: new Date().toISOString(),
|
|
4141
|
+
};
|
|
4142
|
+
return false;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
const tmpQueue = `${queuePath}.workbuddyrebuild.${process.pid}.${Date.now()}`;
|
|
4146
|
+
const tmpCursors = {
|
|
4147
|
+
version: 1,
|
|
4148
|
+
hourly: { buckets: {}, groupQueued: {} },
|
|
4149
|
+
workbuddy: {},
|
|
4150
|
+
};
|
|
4151
|
+
let rebuiltRows = [];
|
|
4152
|
+
try {
|
|
4153
|
+
await parseWorkbuddyIncremental({
|
|
4154
|
+
projectFiles: files,
|
|
4155
|
+
cursors: tmpCursors,
|
|
4156
|
+
queuePath: tmpQueue,
|
|
4157
|
+
env,
|
|
4158
|
+
});
|
|
4159
|
+
let rebuiltRaw = "";
|
|
4160
|
+
try { rebuiltRaw = await fs.readFile(tmpQueue, "utf8"); } catch (e) {
|
|
4161
|
+
if (e?.code !== "ENOENT") throw e;
|
|
4162
|
+
}
|
|
4163
|
+
rebuiltRows = rebuiltRaw.split("\n").filter((line) => line.trim());
|
|
4164
|
+
} catch (e) {
|
|
4165
|
+
console.error("[sync] WorkBuddy context-usage repair: rebuild failed, leaving history untouched:", e?.message || e);
|
|
4166
|
+
return false;
|
|
4167
|
+
} finally {
|
|
4168
|
+
await fs.rm(tmpQueue, { force: true }).catch(() => {});
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
const rebuiltKeys = new Set();
|
|
4172
|
+
const validRebuiltRows = [];
|
|
4173
|
+
for (const line of rebuiltRows) {
|
|
4174
|
+
let row;
|
|
4175
|
+
try { row = JSON.parse(line); } catch (_e) { continue; }
|
|
4176
|
+
if (!row || row.source !== "workbuddy" || typeof row.model !== "string" || typeof row.hour_start !== "string") continue;
|
|
4177
|
+
rebuiltKeys.add(bucketKey("workbuddy", row.model || "unknown", row.hour_start));
|
|
4178
|
+
validRebuiltRows.push(line);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
const staleRetractions = [];
|
|
4182
|
+
for (const key of liveKeys) {
|
|
4183
|
+
if (rebuiltKeys.has(key)) continue;
|
|
4184
|
+
const [, model, ...hourParts] = key.split("|");
|
|
4185
|
+
staleRetractions.push(JSON.stringify({
|
|
4186
|
+
source: "workbuddy",
|
|
4187
|
+
model: model || "unknown",
|
|
4188
|
+
hour_start: hourParts.join("|"),
|
|
4189
|
+
input_tokens: 0,
|
|
4190
|
+
cached_input_tokens: 0,
|
|
4191
|
+
cache_creation_input_tokens: 0,
|
|
4192
|
+
output_tokens: 0,
|
|
4193
|
+
reasoning_output_tokens: 0,
|
|
4194
|
+
total_tokens: 0,
|
|
4195
|
+
billable_total_tokens: 0,
|
|
4196
|
+
conversation_count: 0,
|
|
4197
|
+
}));
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
const nextQueueLines = keptQueueLines.concat(staleRetractions, validRebuiltRows);
|
|
4201
|
+
await ensureDir(path.dirname(queuePath));
|
|
4202
|
+
const queueTmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
|
|
4203
|
+
await fs.writeFile(queueTmp, nextQueueLines.length ? `${nextQueueLines.join("\n")}\n` : "", "utf8");
|
|
4204
|
+
await fs.rename(queueTmp, queuePath);
|
|
4205
|
+
|
|
4206
|
+
const nextHourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
|
|
4207
|
+
nextHourly.buckets ||= {};
|
|
4208
|
+
nextHourly.groupQueued ||= {};
|
|
4209
|
+
for (const key of Object.keys(nextHourly.buckets)) {
|
|
4210
|
+
if (key.startsWith("workbuddy|")) delete nextHourly.buckets[key];
|
|
4211
|
+
}
|
|
4212
|
+
for (const key of Object.keys(nextHourly.groupQueued)) {
|
|
4213
|
+
if (key.startsWith("workbuddy|")) delete nextHourly.groupQueued[key];
|
|
4214
|
+
}
|
|
4215
|
+
for (const [key, bucket] of Object.entries(tmpCursors.hourly.buckets || {})) {
|
|
4216
|
+
if (key.startsWith("workbuddy|")) nextHourly.buckets[key] = bucket;
|
|
4217
|
+
}
|
|
4218
|
+
for (const [key, value] of Object.entries(tmpCursors.hourly.groupQueued || {})) {
|
|
4219
|
+
if (key.startsWith("workbuddy|")) nextHourly.groupQueued[key] = value;
|
|
4220
|
+
}
|
|
4221
|
+
cursors.workbuddy = tmpCursors.workbuddy || {};
|
|
4222
|
+
|
|
4223
|
+
let uploadState = {};
|
|
4224
|
+
try { uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8")); } catch (_e) {}
|
|
4225
|
+
uploadState.offset = 0;
|
|
4226
|
+
uploadState.updatedAt = new Date().toISOString();
|
|
4227
|
+
uploadState.note = "reset_after_workbuddy_context_usage_repair_2026_08";
|
|
4228
|
+
await ensureDir(path.dirname(queueStatePath));
|
|
4229
|
+
await fs.writeFile(queueStatePath, JSON.stringify(uploadState, null, 2) + "\n", "utf8");
|
|
4230
|
+
|
|
4231
|
+
migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
|
|
4232
|
+
status: "applied",
|
|
4233
|
+
appliedAt: new Date().toISOString(),
|
|
4234
|
+
previousBuckets: liveKeys.size,
|
|
4235
|
+
rebuiltBuckets: rebuiltKeys.size,
|
|
4236
|
+
retractedBuckets: staleRetractions.length,
|
|
4237
|
+
};
|
|
4238
|
+
return true;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
3812
4241
|
// One-time repair (#187): rebuild codex hourly buckets that the inode-keyed
|
|
3813
4242
|
// re-scan double-counted before the codexHashes event-dedup landed, and push
|
|
3814
4243
|
// the corrected values to the cloud. Runs BEFORE the codex parse in the same
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
const fssync = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
2
3
|
const wsl = require("./wsl-probe");
|
|
3
4
|
|
|
4
|
-
function resolveInstallPaths({ nativeValue, wslDir, wslValue } = {}, env = process.env, deps = {}) {
|
|
5
|
-
|
|
5
|
+
function resolveInstallPaths({ nativeValue, wslDir, wslValue, requireAnyChild } = {}, env = process.env, deps = {}) {
|
|
6
|
+
const platform = deps.platform || process.platform;
|
|
7
|
+
if (platform !== "win32") {
|
|
6
8
|
return { native: nativeValue ?? null, wsl: null };
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
// requireAnyChild (opt-in): a candidate install must hold at least one of
|
|
12
|
+
// the named children (e.g. "sessions"/"archived_sessions") to qualify — an
|
|
13
|
+
// empty shell dir must not shadow a populated install. For wslDir discovery
|
|
14
|
+
// the check is folded into the existsSync probe so discoverWslHome skips an
|
|
15
|
+
// empty-shell distro and continues to the next one.
|
|
16
|
+
const populated = (p) => !requireAnyChild || hasAnyChild(p, requireAnyChild, deps.existsSync);
|
|
17
|
+
const wslExists = requireAnyChild
|
|
18
|
+
? (p) => Boolean(pathExists(p, deps.existsSync)) && hasAnyChild(p, requireAnyChild, deps.existsSync)
|
|
19
|
+
: deps.existsSync;
|
|
9
20
|
const wslCandidate = wslValue !== undefined
|
|
10
|
-
? (wsl.shouldProbeWsl(env) && pathExists(wslValue, deps.existsSync) ? wslValue : null)
|
|
11
|
-
: (wslDir && wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(wslDir, { ...deps, env }) : null);
|
|
12
|
-
const nativeCandidate = wsl.shouldProbeNative(env) && nativeValue
|
|
21
|
+
? (wsl.shouldProbeWsl(env) && pathExists(wslValue, deps.existsSync) && populated(wslValue) ? wslValue : null)
|
|
22
|
+
: (wslDir && wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(wslDir, { ...deps, env, existsSync: wslExists }) : null);
|
|
23
|
+
const nativeCandidate = wsl.shouldProbeNative(env) && nativeValue && populated(nativeValue)
|
|
13
24
|
? pathExists(nativeValue, deps.existsSync) : null;
|
|
14
25
|
|
|
15
|
-
return wsl.resolveAllWin32Paths({ nativeValue: nativeCandidate, wslValue: wslCandidate, env, platform
|
|
26
|
+
return wsl.resolveAllWin32Paths({ nativeValue: nativeCandidate, wslValue: wslCandidate, env, platform });
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
function pathExists(p, existsSync) {
|
|
@@ -20,6 +31,11 @@ function pathExists(p, existsSync) {
|
|
|
20
31
|
try { return (existsSync || fssync.existsSync)(p) ? p : null; } catch (_e) { return null; }
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
function hasAnyChild(p, children, existsSync) {
|
|
35
|
+
const ex = existsSync || fssync.existsSync;
|
|
36
|
+
return children.some((c) => { try { return ex(path.join(p, c)); } catch (_e) { return false; } });
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
// Migrate a flat (single-install) cursor to { native, wsl } namespaces.
|
|
24
40
|
// `activeKeys` names the namespaces seeded with a copy of the flat state; the
|
|
25
41
|
// others start empty so their install's full history backfills on first parse.
|