tokentracker-cli 1.5.12 → 1.5.14

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.
Files changed (116) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DvcPjqX7.js → AchievementBadge-DqJWve1h.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-C9AkvdHe.js → AchievementsPage-CnuBayj3.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-g0JGNGfT.js → AchievementsSection-WRU5TxAB.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-BHxQ8umD.js → BadgeDetailModal-B5ikEQik.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-C5Byp7Ml.js → BotAnimated-CaMG6j-6.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BV1iTPCd.js → Card-BL3vxENk.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-DcAvPHXS.js → CloudCheckoutPage-CsUb8CJ7.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-pw82pCP6.js → CloudPage-5ZiPFzeC.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-B88IfMHd.js → CommandPalette-CYd9nXW-.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DwtEZ96Z.js → CommunityStatsModal-BItox7Sz.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Cx1-1xUk.js → ConfirmModal-BE1ABR4z.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-hHP6yz1h.js → Controls-BRqIsaHT.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-Cz98astU.js → DashboardPage-Cm0YuBlx.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-BjsUociY.js → DateRangePopover-CRwEu66e.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-C0ZyCHAD.js → DevicePage-DFUPsGb_.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-eeerEmfs.js → DialogClose-DALl4GmP.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-CO0hykis.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-Bn14t_Xd.js → DialogTitle-BSR8ozyn.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-OyoU_h0I.js → ElectricLogo-BtrycBUW.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-BbC4-H2A.js → FadeIn-CfhgvLVd.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-B2CCV29J.js → HeaderGithubStar-N8wqAdOf.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DiD_l4GJ.js → HoverTooltip-CwdMYIzZ.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-1HwHzeVT.js → IpCheckPage-DCRor9cb.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-Cx_xIavm.js → LandingPage-s9xvTHSp.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BsP635cI.js → LeaderboardPage-CUj3WLMo.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-B7uhXYjD.js → LeaderboardProBadge-N4NOKZWj.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-_QOUdhsi.js → LeaderboardProfileModal-09HiUyxm.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CSpx6Rw5.js → LeaderboardProfilePage-Cw9_rodG.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-DOj_2vh6.js → LimitsPage-F8b6kZa0.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DnkG5DmI.js → LocalOnlyNotice-D1eEzDEv.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-Ds9kOBpV.js → LoginCard-0sIovG_J.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B-7fS0gK.js → LoginModal-BT3oMVak.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-B2K2tXvd.js → LoginPage-CDsXdaE5.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DwsDJxX_.js → PetPage-BEnC1azp.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-CqUafQx4.js → PopoverPopup-Dwzp-hyf.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BcE1tBpR.js → ProviderIcon-Cu_lcuuu.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-CP17McXH.js → ResetPasswordPage-DrpEscm1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BcFrd0ex.js → Select-wIR4qvEI.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-C-FBw54p.js → SelectItemText-CNuJgYps.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage--4KcyhvD.js → SelfHostPage-CWNLygpL.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-C4GQ-8Tm.js → ServiceStatusPage-BId9t16Q.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-DMW3U0i3.js → SessionsPage-C_klx7d8.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-sYLabRGu.js → SettingsPage-nC73JaZ4.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CkjmvAds.js → SkillsPage-B2agou8Y.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BfJtQKzg.js → TokenGalaxy-BNXUWRwa.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-BxvShS92.js → TooltipPopup--X2kExQ0.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-C9cHT4G_.js → TrendMonitor-BELHsTnz.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-BqlH3M7h.js → WidgetsPage-DWp0SMXA.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-D2A1zcGs.js → WrappedPage-DgwczKHe.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-RPZeXrAv.js → __vite-browser-external-BHphZRSC.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-CKAwJGNX.js → agent-logos-Cfx5WzU5.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-sB5X7dRY.js → arrow-left-D83SdANw.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-Cvwg139b.js → arrow-right-D7ye9bmJ.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-H5sP370z.js → arrow-up-right-CLmwEwwC.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-B6p2eInB.js → calendar-days-D1qLlvh4.js} +1 -1
  56. package/dashboard/dist/assets/{check-DgFRXvUP.js → check-BHJvi4ey.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-D-ny8IBp.js → chevron-down-6RBPKNpt.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-CT_OgBOA.js → cloud-checkout-DgWcF-at.js} +1 -1
  59. package/dashboard/dist/assets/{composite-BJuoijSA.js → composite-D-0pfyMY.js} +1 -1
  60. package/dashboard/dist/assets/{copy-JdswLd9u.js → copy-B6mJSQsD.js} +1 -1
  61. package/dashboard/dist/assets/{download-BI-n3YEd.js → download-D31Nn-bI.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-CH7tksO4.js → external-link-C_lK9S3p.js} +1 -1
  63. package/dashboard/dist/assets/{flame-CP7hkBXq.js → flame-1ZxhHshT.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-Biw6wloc.js → format-tokens-DJk25SkD.js} +1 -1
  65. package/dashboard/dist/assets/{gift-Dz9MeN8q.js → gift-BUZzFh-1.js} +1 -1
  66. package/dashboard/dist/assets/{icons-DP4Ni8VA.js → icons-BVPThCa4.js} +1 -1
  67. package/dashboard/dist/assets/{index-kQETJkOr.js → index-B-mxrdIz.js} +1 -1
  68. package/dashboard/dist/assets/{index-SXavLLcl.js → index-C0zFsOWh.js} +1 -1
  69. package/dashboard/dist/assets/{index-CIfh9hNs.js → index-CjPsyTl7.js} +1 -1
  70. package/dashboard/dist/assets/{index-BkeoJvCA.js → index-CzstoU6d.js} +1 -1
  71. package/dashboard/dist/assets/{info-Bba3RZJA.js → info-DmU-vlJ6.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-B2C7-GOt.js → link-2-BlBM7uw7.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-Dld8yUT7.js → loader-circle-Dhrb_Ocy.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-CAuRHyVW.js → local-usage-transfer-BudmMtkd.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-CtFVyyHC.js → modifiers.esm-kmw1VM8W.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-iFNG6h_M.js → monitor-smartphone-DtjXNC_x.js} +1 -1
  77. package/dashboard/dist/assets/{play-C7GLNAdX.js → play-9w46lbjy.js} +1 -1
  78. package/dashboard/dist/assets/{plus-CSKQToSA.js → plus-DAf_WGKT.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-BwVQXnb2.js → provider-display-BZ0XlB2q.js} +1 -1
  80. package/dashboard/dist/assets/{react-Cy_Eczdb.js → react-B01ZJuZY.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-DiPfgohh.js → safePolygon-B6_x12Gb.js} +1 -1
  82. package/dashboard/dist/assets/{search-DKyVdlXG.js → search-D3dW9GbV.js} +1 -1
  83. package/dashboard/dist/assets/{server-DaU0lAOr.js → server-C2XZBar7.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-DWiYKLWx.js → shield-check-4FX5vbS7.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-CXotXIRt.js → skills-api-BvW_MUyg.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-ON-92Wzk.js → sortable.esm-CAa_BJgh.js} +1 -1
  87. package/dashboard/dist/assets/{store-C91MN0gm.js → store-2AX2MNkz.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-CZUta1FC.js → terminal-CON7n_ir.js} +1 -1
  89. package/dashboard/dist/assets/{trash-dOvHm24W.js → trash-C2f88jSi.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-BD3O7D_G.js → use-community-stats-CgQu43xV.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-Geg2sygc.js → use-latest-request-guard-lShM46-3.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-Ck2CuPqb.js → use-limits-display-prefs-8o7pbEg0.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-3DH5xldB.js → use-native-settings-dN8shG3M.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-BonkP7AB.js → use-ordered-list-beJy2IqQ.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-S0bR_G1B.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-C32HObUN.js → use-transform-cJpAQr2d.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-6GFRuUdV.js → use-usage-limits-CuusZA6b.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-BsGBVGb5.js → useCurrency-Cx9UsKJK.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-DvFrrUs0.js → usePositioner-TEClK6mF.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-Dryg74BA.js → useTokenFormat-BNwTLiac.js} +1 -1
  101. package/dashboard/dist/assets/{zap-BL_hxkkj.js → zap-CFgbVEhC.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/commands/init.js +30 -16
  108. package/src/commands/serve.js +28 -25
  109. package/src/commands/sync.js +3 -3
  110. package/src/lib/cursor-store.js +5 -1
  111. package/src/lib/local-api.js +25 -11
  112. package/src/lib/pricing/seed-snapshot.json +1 -1
  113. package/src/lib/rollout.js +262 -59
  114. package/src/lib/runtime-copy.js +77 -0
  115. package/dashboard/dist/assets/DialogDescription-DG_4qSj0.js +0 -1
  116. package/dashboard/dist/assets/use-reduced-motion-uryC1l51.js +0 -1
@@ -11,7 +11,9 @@ const {
11
11
  readJson,
12
12
  writeJson,
13
13
  chmod600IfPossible,
14
+ openLock,
14
15
  } = require("../lib/fs");
16
+ const { copyRuntimeTree, ensureRuntimeDirectory } = require("../lib/runtime-copy");
15
17
  const { resolveMimoNativeDbPath } = require("../lib/install-resolver");
16
18
  const { resolveDumateDbPaths } = require("../lib/dumate");
17
19
  const { expandHome } = require("../lib/scan-roots");
@@ -2291,7 +2293,7 @@ async function installLocalTrackerApp({ appDir }) {
2291
2293
  // Copy the current package's runtime (bin + src) into ~/.tokentracker so notify can run sync without npx.
2292
2294
  const packageRoot = path.resolve(__dirname, "../..");
2293
2295
  const srcFrom = path.join(packageRoot, "src");
2294
- const binFrom = path.join(packageRoot, "bin", "tracker.js");
2296
+ const binFrom = path.join(packageRoot, "bin");
2295
2297
  const packageJsonFrom = path.join(packageRoot, "package.json");
2296
2298
  const nodeModulesFrom = path.join(packageRoot, "node_modules");
2297
2299
  const dashboardDistFrom = path.join(packageRoot, "dashboard", "dist");
@@ -2308,17 +2310,31 @@ async function installLocalTrackerApp({ appDir }) {
2308
2310
  const nodeModulesTo = path.join(appDir, "node_modules");
2309
2311
  const dashboardDistTo = path.join(appDir, "dashboard", "dist");
2310
2312
 
2311
- await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
2312
- await ensureDir(appDir);
2313
- await fs.cp(srcFrom, srcTo, { recursive: true });
2314
- await ensureDir(binToDir);
2315
- await fs.copyFile(binFrom, binTo);
2316
- await fs.chmod(binTo, 0o755).catch(() => {});
2317
- await fs.copyFile(packageJsonFrom, path.join(appDir, "package.json")).catch(() => {});
2318
- if (await isDir(dashboardDistFrom)) {
2319
- await fs.cp(dashboardDistFrom, dashboardDistTo, { recursive: true });
2313
+ // Serialize installers: init and desktop startup can overlap. In particular,
2314
+ // one refresh must not prune another writer's atomic temporary files.
2315
+ await ensureDir(path.dirname(appDir));
2316
+ const deadline = Date.now() + 120_000;
2317
+ let lock;
2318
+ while (!(lock = await openLock(`${appDir}.install.lock`, { quietIfLocked: true }))) {
2319
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for local runtime installation");
2320
+ await new Promise((resolve) => setTimeout(resolve, 100));
2321
+ }
2322
+ try {
2323
+ await ensureRuntimeDirectory(appDir);
2324
+ await copyRuntimeTree(srcFrom, srcTo);
2325
+ await copyRuntimeTree(binFrom, binToDir);
2326
+ await fs.chmod(binTo, 0o755).catch(() => {});
2327
+ if (await isDir(dashboardDistFrom)) {
2328
+ await ensureRuntimeDirectory(path.dirname(dashboardDistTo));
2329
+ await copyRuntimeTree(dashboardDistFrom, dashboardDistTo);
2330
+ } else {
2331
+ await fs.rm(path.dirname(dashboardDistTo), { recursive: true, force: true });
2332
+ }
2333
+ await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
2334
+ await copyRuntimeTree(packageJsonFrom, path.join(appDir, "package.json"));
2335
+ } finally {
2336
+ await lock.release();
2320
2337
  }
2321
- await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
2322
2338
  }
2323
2339
 
2324
2340
  async function pathsPointToSameLocation(a, b) {
@@ -2427,15 +2443,13 @@ function readFirstSyncTotals(trackerDir) {
2427
2443
  }
2428
2444
 
2429
2445
  async function copyRuntimeDependencies({ from, to }) {
2430
- try {
2431
- const st = await fs.stat(from);
2432
- if (!st.isDirectory()) return;
2433
- } catch (_e) {
2446
+ if (!(await isDir(from))) {
2447
+ await fs.rm(to, { recursive: true, force: true });
2434
2448
  return;
2435
2449
  }
2436
2450
 
2437
2451
  try {
2438
- await fs.cp(from, to, { recursive: true });
2452
+ await copyRuntimeTree(from, to);
2439
2453
  } catch (_e) {
2440
2454
  // Best-effort: missing dependencies will fall back to npx at notify time.
2441
2455
  }
@@ -83,32 +83,9 @@ async function cmdServe(argv) {
83
83
  // the setup never commits cursors.json (#961).
84
84
  const { trackerDir, binDir } = await resolveTrackerPaths();
85
85
 
86
- try {
87
- const { installLocalTrackerApp, repairRuntimeIntegrations } = require("./init");
88
- await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
89
- const repairResult = await repairRuntimeIntegrations({ trackerDir, binDir, safeMode: true });
90
- for (const warning of repairResult?.warnings || []) {
91
- process.stdout.write(
92
- `Runtime integration repair warning (${warning.integration}): ${warning.error}\n`,
93
- );
94
- }
95
- } catch (e) {
96
- process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
97
- }
98
-
99
- // Optional sync for non-first-run launches. First-run init still owns its
100
- // inline first sync below, after the listener is up.
86
+ // All runtime maintenance and initial sync run after listen. The dashboard
87
+ // is served from this package, not the hook runtime installed in trackerDir.
101
88
  const needsFirstRunSetup = !fssync.existsSync(path.join(trackerDir, "cursors.json"));
102
- if (opts.sync && !needsFirstRunSetup) {
103
- process.stdout.write("Syncing local data...\n");
104
- try {
105
- const { cmdSync } = require("./sync");
106
- await cmdSync(["--auto"]);
107
- process.stdout.write("Sync done.\n");
108
- } catch (e) {
109
- process.stdout.write(`Sync warning: ${e?.message || e}\n`);
110
- }
111
- }
112
89
 
113
90
  // Resolve paths
114
91
  const queuePath = resolveQueuePath();
@@ -240,6 +217,32 @@ async function cmdServe(argv) {
240
217
  }
241
218
  }
242
219
 
220
+ // A large Windows runtime copy can take tens of seconds. Health probes and
221
+ // the dashboard must already work while maintenance is in progress.
222
+ try {
223
+ const { installLocalTrackerApp, repairRuntimeIntegrations } = require("./init");
224
+ await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
225
+ const repairResult = await repairRuntimeIntegrations({ trackerDir, binDir, safeMode: true });
226
+ for (const warning of repairResult?.warnings || []) {
227
+ process.stdout.write(
228
+ `Runtime integration repair warning (${warning.integration}): ${warning.error}\n`,
229
+ );
230
+ }
231
+ } catch (e) {
232
+ process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
233
+ }
234
+
235
+ if (opts.sync && !needsFirstRunSetup) {
236
+ process.stdout.write("Syncing local data...\n");
237
+ try {
238
+ const { cmdSync } = require("./sync");
239
+ await cmdSync(["--auto"]);
240
+ process.stdout.write("Sync done.\n");
241
+ } catch (e) {
242
+ process.stdout.write(`Sync warning: ${e?.message || e}\n`);
243
+ }
244
+ }
245
+
243
246
  // First-time setup AFTER the listener is accepting health probes. Init still
244
247
  // runs inline (including first sync) so the dashboard fills in, but native
245
248
  // hosts can mark the server Running without racing that sync (#961).
@@ -315,7 +315,7 @@ const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
315
315
  const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
316
316
  const NOTIFY_LOCK_WAIT_MS = 130_000;
317
317
  const NOTIFY_LOCK_POLL_MS = 5_000;
318
- // Local API sync requests have a 120-second child-process budget. Reserve at
318
+ // Local API sync requests answer within a 120-second wait window. Reserve at
319
319
  // least 30 seconds for parsing, persistence, and the response after contention.
320
320
  const PRIORITY_LOCK_WAIT_MS = 90_000;
321
321
  const PRIORITY_LOCK_POLL_MS = 250;
@@ -1854,7 +1854,7 @@ async function cmdSync(argv, context = {}) {
1854
1854
  if (sourceAllowed("dsh")) {
1855
1855
  await migrateLegacyDeepseekHarnessSource({ cursors, queuePath, queueStatePath });
1856
1856
  const dshSessionFiles = await resolveDshSessionFiles(process.env);
1857
- if (dshSessionFiles.length > 0) {
1857
+ if (dshSessionFiles.length > 0 || cursors.dsh) {
1858
1858
  if (progress?.enabled) {
1859
1859
  progress.start(
1860
1860
  `Parsing DeepSeek Harness ${renderBar(0)} 0/${formatNumber(
@@ -1873,7 +1873,7 @@ async function cmdSync(argv, context = {}) {
1873
1873
  warnProviderParseFailure(
1874
1874
  "DeepSeek Harness",
1875
1875
  new Error(
1876
- `${dshResult.deferredMigrations} artifact migration(s) deferred; inspect cursors.dsh.deferredMigrations and retry after the replacement is complete`,
1876
+ `${dshResult.deferredMigrations} accounting repair/artifact migration(s) deferred; inspect cursors.dsh.deferredMigrations, restore unavailable history or complete the replacement, then retry`,
1877
1877
  ),
1878
1878
  opts,
1879
1879
  );
@@ -1850,9 +1850,13 @@ async function fingerprintFile(filePath) {
1850
1850
  }
1851
1851
  }
1852
1852
 
1853
+ // Legacy cursors.json is re-imported only when its content may have changed.
1854
+ // ctime moves on metadata-only events (chmod, xattr, backup tools); comparing
1855
+ // it re-imported a stale snapshot over newer v2 state and forced a full
1856
+ // re-read of every session log.
1853
1857
  function sameFingerprint(left, right) {
1854
1858
  if (!left || !right) return left === right;
1855
- return ["dev", "ino", "size", "mtimeNs", "ctimeNs"]
1859
+ return ["dev", "ino", "size", "mtimeNs"]
1856
1860
  .every((key) => String(left[key]) === String(right[key]));
1857
1861
  }
1858
1862
 
@@ -26,7 +26,9 @@ const { negotiateEncoding } = require("./accept-encoding");
26
26
  const { createLocalUsageTransfer, MAX_BYTES: LOCAL_USAGE_MAX_BYTES } = require("./local-usage-transfer");
27
27
  const { fetchProviderServiceStatus } = require("./provider-status");
28
28
 
29
- const SYNC_TIMEOUT_MS = 120_000;
29
+ // How long a local-sync request waits for its sync child before answering.
30
+ // Native clients give up at 130s (macOS) so the server must answer first.
31
+ const SYNC_RESPONSE_WAIT_MS = 120_000;
30
32
  // A failed account-view request should not stall every dashboard refresh
31
33
  // while an expired session or unreachable backend keeps timing out. The first
32
34
  // request still falls back normally; subsequent fan-out requests reuse that
@@ -910,15 +912,9 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
910
912
  const finish = (fn, v) => {
911
913
  if (settled) return;
912
914
  settled = true;
913
- clearTimeout(tid);
914
915
  opts.signal?.removeEventListener("abort", cancel);
915
916
  fn(v);
916
917
  };
917
- let timedOut = false;
918
- const tid = setTimeout(() => {
919
- timedOut = true;
920
- child.kill("SIGTERM");
921
- }, SYNC_TIMEOUT_MS);
922
918
  child.stdout?.on("data", (c) => {
923
919
  stdout += c;
924
920
  });
@@ -929,7 +925,6 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
929
925
  finish(reject, Object.assign(e, { stdout: trimOutput(stdout), stderr: trimOutput(stderr) }));
930
926
  });
931
927
  child.on("close", (code) => {
932
- if (timedOut) { finish(reject, Object.assign(new Error("Sync timed out"), { code: "SYNC_TIMEOUT", stdout: trimOutput(stdout), stderr: trimOutput(stderr) })); return; }
933
928
  if (opts.signal?.aborted) { finish(reject, Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 })); return; }
934
929
  const r = { code: code ?? 1, stdout: trimOutput(stdout), stderr: trimOutput(stderr) };
935
930
  if (code === 0) {
@@ -948,6 +943,23 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
948
943
  });
949
944
  }
950
945
 
946
+ // The sync child owns its own lifetime: it holds the sync lock and commits its
947
+ // cursors only after the whole scan finishes. Killing it when the HTTP caller
948
+ // stops waiting discarded that progress, so a backlog longer than one wait
949
+ // window was rescanned and killed on every attempt and never landed. The
950
+ // caller gets an answer after `waitMs`; the child keeps running to completion
951
+ // and only an explicit abort (account session change) terminates it.
952
+ function waitForSyncResponse(syncPromise, waitMs = SYNC_RESPONSE_WAIT_MS) {
953
+ let tid;
954
+ const expired = new Promise((_, reject) => {
955
+ tid = setTimeout(() => reject(Object.assign(
956
+ new Error("Sync is still running in the background"),
957
+ { code: "SYNC_RUNNING", status: 503 },
958
+ )), waitMs);
959
+ });
960
+ return Promise.race([syncPromise, expired]).finally(() => clearTimeout(tid));
961
+ }
962
+
951
963
  // ---------------------------------------------------------------------------
952
964
  // Project detection helpers
953
965
  // ---------------------------------------------------------------------------
@@ -2577,13 +2589,15 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2577
2589
  waitForLock:
2578
2590
  !drain && !background && Boolean(extraEnv.TOKENTRACKER_DEVICE_TOKEN),
2579
2591
  });
2580
- controller.done = syncPromise.catch(() => {});
2581
- const result = await syncPromise.catch((error) => {
2592
+ // The controller tracks the child, not this request: an account switch
2593
+ // must still be able to stop a sync that outlived its HTTP wait.
2594
+ controller.done = syncPromise.catch(() => {}).finally(() => activeCloudSyncs.delete(controller));
2595
+ const result = await waitForSyncResponse(syncPromise).catch((error) => {
2582
2596
  if (uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
2583
2597
  const failure = currentCloudUploadFailure(syncStartedAt, { backoff: auto && drain, deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
2584
2598
  if (failure) Object.assign(error, failure);
2585
2599
  throw error;
2586
- }).finally(() => activeCloudSyncs.delete(controller));
2600
+ });
2587
2601
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
2588
2602
  const failure = currentCloudUploadFailure(syncStartedAt, { deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
2589
2603
  if (failure) throw Object.assign(new Error("Cloud upload failed"), failure);