tokentracker-cli 0.87.14 → 0.87.16
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/README.de.md +2 -2
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +3 -3
- package/README.zh-CN.md +2 -2
- package/dashboard/dist/assets/{AchievementBadge-D06ffgXR.js → AchievementBadge-DaKHLTL2.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-D6uGfLgx.js → AchievementsPage-DJdtjodG.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Chjq16ns.js → AchievementsSection-DVtxGDIP.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D42zXSN1.js → BadgeDetailModal-CF2dlQkT.js} +1 -1
- package/dashboard/dist/assets/{Card-DGOEtq_3.js → Card-DuQWS5aB.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-B6HFLXEb.js → ClawdAnimated-l8AN6_pc.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-1tyBT_Gf.js → CommandPalette-DWKyGhWG.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Cvbk67o1.js → CommunityStatsModal-CoJ9i3QH.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-RHQRpQG1.js → ConfirmModal-DM6A6p4g.js} +1 -1
- package/dashboard/dist/assets/{Controls-WATRUYwX.js → Controls-DBUMibne.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-B-jHLRu6.js → DashboardPage-BgVXjT0v.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CgYxNWgU.js → DevicePage-Beby6BhC.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CIQziRo4.js → DialogClose-nwx-N3WL.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BFVB627n.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DG5OaNy0.js → DialogTitle-Cw53obf0.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BmtyZXeb.js → FadeIn-CTTwoVh-.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cg7Ap56b.js → HeaderGithubStar-BvaEHM9B.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Dh1v-yph.js → HoverTooltip-CbJqz6cF.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CqQqdE3D.js → IpCheckPage-I0REWeew.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Ct4m67m9.js → LandingPage-CuMWPQBC.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-3ZA9tDrh.js → LeaderboardAvatar-MHfIP4_U.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CQZf8G8t.js → LeaderboardPage-DfDM-dH9.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-KaaGOK3_.js → LeaderboardProfileModal-hmpU-JV7.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DsiYvLlz.js → LeaderboardProfilePage-DtUb4dDZ.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-CtA0c2G5.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-DMrkrlZY.js → LocalOnlyNotice-WI3Oke4U.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BFQJZmKV.js → LoginCard-DMprjRI7.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-Dx4_4UfW.js → LoginModal-DffmK0ro.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-AmxdJOiw.js → LoginPage-B2ZgB6K7.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DbMcLOoc.js → PetPage-D_U7yXgF.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CwOPhh6g.js → PopoverPopup-Ph-9l9jU.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-C2183DbB.js → ProviderIcon-D2s_KSYC.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CwQt9Jh0.js → ResetPasswordPage-Cpg2C8BI.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-2gfEdOHR.js → SegmentedControl-C89jlPzS.js} +1 -1
- package/dashboard/dist/assets/{Select-BknJIua6.js → Select-BLPgobGl.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BNfM6qJ0.js → SelectItemText-CLTEQqQZ.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-_C25Txd5.js → ServiceStatusPage-Bd1ec7bz.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-B_QkotAc.js → SessionsPage-HPbQDLZ5.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B-vif0ea.js → SettingsPage-CwfcRYYu.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-BX2rApOM.js → SkillsPage-DflhF8pa.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-C9LrhOJ1.js → TokenGalaxy-CylVSV2J.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-Bevy5CSh.js → ToolbarRootContext-BzBAJ8C5.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-C6axjU1E.js → TrendMonitor-B0IzqLaN.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-gmpxGRU-.js → WidgetsPage-Bvt5JCoS.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Lyyj4Lfy.js → WrappedPage-BXjDpjD_.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DsFkaOPr.js → __vite-browser-external-CDv96ZcG.js} +14 -11
- package/dashboard/dist/assets/{agent-logos-CmI9Jrx-.js → agent-logos-bPIt1QVl.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BDsE7MjI.js → arrow-up-right-C_6NrnRB.js} +1 -1
- package/dashboard/dist/assets/{check-Dv03s0Oi.js → check-ovHAYNwt.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-Bg0LlTK2.js → chevron-right-BeHvjzPf.js} +1 -1
- package/dashboard/dist/assets/{copy-M7jcXrOp.js → copy-DRQHwFKm.js} +1 -1
- package/dashboard/dist/assets/{download-B7-6bJLc.js → download-qPfSWDyf.js} +1 -1
- package/dashboard/dist/assets/{external-link-C2dWQL59.js → external-link-t7Qbyp7r.js} +1 -1
- package/dashboard/dist/assets/{flame-fuODmpET.js → flame-DlRdMOxO.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BKb3uo9Y.js → format-tokens-DP1DrOxZ.js} +1 -1
- package/dashboard/dist/assets/{icons-CZ3rB5D9.js → icons-DBgKdjpV.js} +1 -1
- package/dashboard/dist/assets/{index-CMfErAEy.js → index-BFaAjCwg.js} +1 -1
- package/dashboard/dist/assets/{index-CyzVAu8L.js → index-C1wNjCL_.js} +1 -1
- package/dashboard/dist/assets/{index-DSLv7Mj8.js → index-Dtzl1NhK.js} +1 -1
- package/dashboard/dist/assets/{info-gtQU6mdi.js → info-D9fn_ZQQ.js} +1 -1
- package/dashboard/dist/assets/limits-providers-JyTKN-3e.js +1 -0
- package/dashboard/dist/assets/{link-2-DAoauLvL.js → link-2-CmnuQiZs.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C6-hnHM5.js → loader-circle-Bm3FqDLX.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DBWhzAmU.js → maximize-2-CutKDynW.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Cg5lp-7f.js → modifiers.esm-C94MH21S.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-BVkQLoJM.js → monitor-smartphone-wSLhNWq5.js} +1 -1
- package/dashboard/dist/assets/{provider-display-N7MWKGSI.js → provider-display-CR8o_M3v.js} +1 -1
- package/dashboard/dist/assets/{react-DDPYgpAM.js → react-Cc1Ca2uE.js} +1 -1
- package/dashboard/dist/assets/{search-M9zIhfTN.js → search-BXeDDAWS.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DPJzchQg.js → skills-api-DBP8kM7W.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-B1tZenH5.js → sortable.esm-Cd_ODlnX.js} +1 -1
- package/dashboard/dist/assets/{store-DmPQtoHI.js → store-lmptvFHG.js} +1 -1
- package/dashboard/dist/assets/{terminal-BpvBtoDx.js → terminal-965BMhR0.js} +1 -1
- package/dashboard/dist/assets/{trash-2-XKMDw3cY.js → trash-2-wq5YeaAT.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-B1BMhURp.js → use-community-stats-BCIgHdTZ.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DaZnSXop.js → use-limits-display-prefs-D0MB2ewv.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DKa6gE-O.js → use-ordered-list-DrQsh022.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DhNppA5X.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-B9I7_v4k.js → use-session-efficiency-pref-BeBSADa-.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Ce5ESEGX.js → use-transform-fRpwp-G3.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Ex51qVV9.js → use-usage-limits-C0Tk-ghm.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-8kmm6K74.js → useCurrency-BAZNRyKh.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-D3ximygL.js → useScrollLock-CU659jBG.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BtVvEcPK.js → useTokenFormat-l0Xd62a5.js} +1 -1
- package/dashboard/dist/assets/{zap-y-504T0t.js → zap-CWFkq16a.js} +1 -1
- package/dashboard/dist/index.html +4 -4
- 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/serve.js +11 -3
- package/src/commands/status.js +34 -0
- package/src/commands/sync.js +95 -1
- package/src/lib/diagnostics.js +10 -0
- package/src/lib/fs.js +79 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/qoder-limits.js +89 -34
- package/src/lib/rollout.js +226 -43
- package/src/lib/session-analytics.js +45 -5
- package/src/lib/usage-limits.js +93 -14
- package/dashboard/dist/assets/DialogDescription-CMZSKLXB.js +0 -1
- package/dashboard/dist/assets/LimitsPage-Dv1fg4Jh.js +0 -4
- package/dashboard/dist/assets/limits-providers-B9o4daVn.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-fPvY8iYM.js +0 -1
package/src/commands/status.js
CHANGED
|
@@ -66,6 +66,7 @@ const {
|
|
|
66
66
|
resolveRoocodeTaskFiles,
|
|
67
67
|
resolveZedDbPath,
|
|
68
68
|
resolveQoderDbPaths,
|
|
69
|
+
resolveQoderCnDbPaths,
|
|
69
70
|
resolveClaudeScienceDbPaths,
|
|
70
71
|
resolveAnythingllmDbPath,
|
|
71
72
|
resolveGooseDbPath,
|
|
@@ -118,6 +119,7 @@ async function cmdStatus(argv = []) {
|
|
|
118
119
|
const throttlePath = path.join(trackerDir, "sync.throttle");
|
|
119
120
|
const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
|
|
120
121
|
const autoRetryPath = path.join(trackerDir, "auto.retry.json");
|
|
122
|
+
const syncSkipPath = path.join(trackerDir, "sync.skip.json");
|
|
121
123
|
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
122
124
|
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
123
125
|
const codeHome = process.env.CODE_HOME || path.join(home, ".code");
|
|
@@ -153,6 +155,9 @@ async function cmdStatus(argv = []) {
|
|
|
153
155
|
await readJson(uploadThrottlePath),
|
|
154
156
|
);
|
|
155
157
|
const autoRetry = await readJson(autoRetryPath);
|
|
158
|
+
// Written by sync whenever it had to skip a run. Present here so a stalled
|
|
159
|
+
// parse has a visible cause instead of only a frozen "Last parse".
|
|
160
|
+
const syncSkip = await readJson(syncSkipPath);
|
|
156
161
|
|
|
157
162
|
const queueSize = await safeStatSize(queuePath);
|
|
158
163
|
const pendingBytes = Math.max(0, queueSize - (queueState.offset || 0));
|
|
@@ -212,6 +217,11 @@ async function cmdStatus(argv = []) {
|
|
|
212
217
|
const lastUploadError = uploadThrottle.lastError
|
|
213
218
|
? `${uploadThrottle.lastErrorAt || "unknown"} ${uploadThrottle.lastError}`
|
|
214
219
|
: null;
|
|
220
|
+
const syncSkipLine = syncSkip?.at
|
|
221
|
+
? `- Last sync skipped: ${syncSkip.at} (${syncSkip.reason || "unknown"}${
|
|
222
|
+
syncSkip.detail ? `, ${syncSkip.detail}` : ""
|
|
223
|
+
})`
|
|
224
|
+
: null;
|
|
215
225
|
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs || null);
|
|
216
226
|
const autoRetryLine = autoRetryAt
|
|
217
227
|
? `- Auto retry after: ${autoRetryAt} (${autoRetry?.reason || "scheduled"}, pending ${Number(
|
|
@@ -428,6 +438,16 @@ async function cmdStatus(argv = []) {
|
|
|
428
438
|
const qoderInstalled = qoderActive.length > 0;
|
|
429
439
|
const qoderDbPath = qoderActive.join(" | ");
|
|
430
440
|
|
|
441
|
+
// Qoder CN (国内版) — same schema, separate Application Support/QoderCN dir.
|
|
442
|
+
const qoderCnPaths = resolveQoderCnDbPaths({
|
|
443
|
+
home,
|
|
444
|
+
env: process.env,
|
|
445
|
+
platform: process.platform,
|
|
446
|
+
});
|
|
447
|
+
const qoderCnActive = formatResolvedPaths(qoderCnPaths);
|
|
448
|
+
const qoderCnInstalled = qoderCnActive.length > 0;
|
|
449
|
+
const qoderCnDbPath = qoderCnActive.join(" | ");
|
|
450
|
+
|
|
431
451
|
// Claude Science — token usage lives on the `frames` table of operon-cli.db.
|
|
432
452
|
// Unlike the native/WSL pair other providers resolve to, this is an open-ended
|
|
433
453
|
// list: multi-org installs keep one DB per org (and on Windows they all sit
|
|
@@ -731,6 +751,7 @@ async function cmdStatus(argv = []) {
|
|
|
731
751
|
next_upload_after: nextUpload || null,
|
|
732
752
|
backoff_until: backoffUntil || null,
|
|
733
753
|
last_upload_error: lastUploadError || null,
|
|
754
|
+
last_sync_skipped: syncSkip?.at ? syncSkip : null,
|
|
734
755
|
auto_retry: autoRetry || null,
|
|
735
756
|
hooks: {
|
|
736
757
|
codex_notify: notifyConfigured,
|
|
@@ -802,6 +823,9 @@ async function cmdStatus(argv = []) {
|
|
|
802
823
|
qoder: qoderInstalled
|
|
803
824
|
? { installed: true, detail: qoderDbPath }
|
|
804
825
|
: { installed: false },
|
|
826
|
+
"qoder-cn": qoderCnInstalled
|
|
827
|
+
? { installed: true, detail: qoderCnDbPath }
|
|
828
|
+
: { installed: false },
|
|
805
829
|
"claude-science": claudeScienceInstalled
|
|
806
830
|
? { installed: true, detail: claudeScienceDbPath }
|
|
807
831
|
: { installed: false },
|
|
@@ -875,6 +899,7 @@ async function cmdStatus(argv = []) {
|
|
|
875
899
|
`- Next upload after: ${nextUpload || "never"}`,
|
|
876
900
|
`- Backoff until: ${backoffUntil || "never"}`,
|
|
877
901
|
lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
|
|
902
|
+
syncSkipLine,
|
|
878
903
|
autoRetryLine,
|
|
879
904
|
`- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : "unset"}`,
|
|
880
905
|
`- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : "unset"}`,
|
|
@@ -926,6 +951,9 @@ async function cmdStatus(argv = []) {
|
|
|
926
951
|
qoderInstalled
|
|
927
952
|
? `- Qoder: passive reader (${qoderDbPath})`
|
|
928
953
|
: null,
|
|
954
|
+
qoderCnInstalled
|
|
955
|
+
? `- Qoder CN: passive reader (${qoderCnDbPath})`
|
|
956
|
+
: null,
|
|
929
957
|
claudeScienceInstalled
|
|
930
958
|
? `- Claude Science: passive reader (${claudeScienceDbPath})`
|
|
931
959
|
: null,
|
|
@@ -1133,6 +1161,12 @@ function renderLightTable(summary) {
|
|
|
1133
1161
|
push("Next upload after", summary.next_upload_after);
|
|
1134
1162
|
push("Backoff until", summary.backoff_until);
|
|
1135
1163
|
if (summary.last_upload_error) push("Last upload error", summary.last_upload_error);
|
|
1164
|
+
if (summary.last_sync_skipped) {
|
|
1165
|
+
push(
|
|
1166
|
+
"Last sync skipped",
|
|
1167
|
+
`${summary.last_sync_skipped.at} (${summary.last_sync_skipped.reason || "unknown"})`,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1136
1170
|
|
|
1137
1171
|
for (const [name, state] of Object.entries(summary.hooks || {})) {
|
|
1138
1172
|
push(`Hook · ${name}`, state ? "set" : "unset");
|
package/src/commands/sync.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
writeJson,
|
|
15
15
|
chmod600IfPossible,
|
|
16
16
|
openLock,
|
|
17
|
+
inspectLock,
|
|
17
18
|
} = require("../lib/fs");
|
|
18
19
|
const { physicalJsonlRecords } = require("../lib/jsonl-lines");
|
|
19
20
|
const {
|
|
@@ -28,6 +29,7 @@ const {
|
|
|
28
29
|
readMimoDbMessages,
|
|
29
30
|
readZcodeDbMessages,
|
|
30
31
|
resolveQoderDbPaths,
|
|
32
|
+
resolveQoderCnDbPaths,
|
|
31
33
|
readQoderDbMessages,
|
|
32
34
|
resolveKiroDbPath,
|
|
33
35
|
resolveKiroJsonlPath,
|
|
@@ -277,6 +279,7 @@ const AUTO_SYNC_SOURCES = new Set([
|
|
|
277
279
|
"openclaw",
|
|
278
280
|
"pi",
|
|
279
281
|
"qoder",
|
|
282
|
+
"qoder-cn",
|
|
280
283
|
"roocode",
|
|
281
284
|
"workbuddy",
|
|
282
285
|
"zcode",
|
|
@@ -397,6 +400,38 @@ async function acquireSyncLock(
|
|
|
397
400
|
}
|
|
398
401
|
}
|
|
399
402
|
|
|
403
|
+
const SYNC_SKIP_MARKER = "sync.skip.json";
|
|
404
|
+
|
|
405
|
+
// A skipped sync used to exit 0 with no output whatsoever, so lock debris left
|
|
406
|
+
// by a killed run stalled every parse for a day with no visible signal and
|
|
407
|
+
// nothing for `tokentracker status` to report (issue #431). Leave a queryable
|
|
408
|
+
// marker that separates ordinary contention (`lock_busy`) from a lease whose
|
|
409
|
+
// owner is gone (`lock_debris`, the state that needs attention).
|
|
410
|
+
async function recordSyncSkip(trackerDir, lockPath) {
|
|
411
|
+
const holder = await inspectLock(lockPath);
|
|
412
|
+
const reason = holder.exists && holder.pid && !holder.alive ? "lock_debris" : "lock_busy";
|
|
413
|
+
const detail = !holder.exists
|
|
414
|
+
? "sync lock could not be acquired"
|
|
415
|
+
: holder.pid
|
|
416
|
+
? `sync lock held by pid ${holder.pid} (${holder.alive ? "running" : "not running"})`
|
|
417
|
+
: "sync lock has no owner record";
|
|
418
|
+
await writeJson(path.join(trackerDir, SYNC_SKIP_MARKER), {
|
|
419
|
+
reason,
|
|
420
|
+
detail,
|
|
421
|
+
at: new Date().toISOString(),
|
|
422
|
+
lockPath,
|
|
423
|
+
});
|
|
424
|
+
return `Sync skipped: ${reason} — ${detail}\n`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function clearSyncSkip(trackerDir) {
|
|
428
|
+
try {
|
|
429
|
+
await fs.unlink(path.join(trackerDir, SYNC_SKIP_MARKER));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (error?.code !== "ENOENT") throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
400
435
|
async function cmdSync(argv, context = {}) {
|
|
401
436
|
const opts = parseArgs(argv);
|
|
402
437
|
const diagnostics = context && typeof context === "object" ? context.diagnostics : null;
|
|
@@ -417,7 +452,13 @@ async function cmdSync(argv, context = {}) {
|
|
|
417
452
|
|
|
418
453
|
const lockPath = path.join(trackerDir, "sync.lock");
|
|
419
454
|
const lock = await acquireSyncLock(lockPath, opts, lockWaitOptions);
|
|
420
|
-
if (!lock)
|
|
455
|
+
if (!lock) {
|
|
456
|
+
// Warn on stderr so the notice reaches interactive and background runs
|
|
457
|
+
// alike without disturbing anything that parses sync's stdout.
|
|
458
|
+
process.stderr.write(await recordSyncSkip(trackerDir, lockPath));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
await clearSyncSkip(trackerDir);
|
|
421
462
|
|
|
422
463
|
let progress = null;
|
|
423
464
|
try {
|
|
@@ -1159,6 +1200,57 @@ async function cmdSync(argv, context = {}) {
|
|
|
1159
1200
|
}
|
|
1160
1201
|
}
|
|
1161
1202
|
|
|
1203
|
+
// ── Qoder CN (国内版) — same SharedClientCache/local.db schema, separate
|
|
1204
|
+
// Application Support/QoderCN data directory. Tracked as its own source
|
|
1205
|
+
// with its own cursor namespace: the two DBs each number rowids from 1, so
|
|
1206
|
+
// a shared cursor would mis-dedup across installs.
|
|
1207
|
+
let qoderCnResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
1208
|
+
if (sourceAllowed("qoder-cn")) {
|
|
1209
|
+
const qoderCnPaths = resolveQoderCnDbPaths({
|
|
1210
|
+
home,
|
|
1211
|
+
env: process.env,
|
|
1212
|
+
platform: process.platform,
|
|
1213
|
+
});
|
|
1214
|
+
if (qoderCnPaths.native || qoderCnPaths.wsl) {
|
|
1215
|
+
if (progress?.enabled) {
|
|
1216
|
+
progress.start(`Parsing Qoder CN ${renderBar(0)} | buckets 0`);
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
const result = await multiInstallParse({
|
|
1220
|
+
paths: qoderCnPaths,
|
|
1221
|
+
parserFn: async ({ dbPath, ...rest }) => {
|
|
1222
|
+
const dbMessages = await readQoderDbMessages(dbPath, { label: "Qoder CN" });
|
|
1223
|
+
const parsed = await parseQoderDbIncremental({
|
|
1224
|
+
dbMessages,
|
|
1225
|
+
dbPath,
|
|
1226
|
+
sourceKey: "qoder-cn",
|
|
1227
|
+
cursorKey: "qoder-cn",
|
|
1228
|
+
...rest,
|
|
1229
|
+
});
|
|
1230
|
+
return {
|
|
1231
|
+
recordsProcessed: parsed.messagesProcessed || 0,
|
|
1232
|
+
eventsAggregated: parsed.eventsAggregated || 0,
|
|
1233
|
+
bucketsQueued: parsed.bucketsQueued || 0,
|
|
1234
|
+
};
|
|
1235
|
+
},
|
|
1236
|
+
providerName: "qoder-cn",
|
|
1237
|
+
cursors,
|
|
1238
|
+
queuePath,
|
|
1239
|
+
projectQueuePath,
|
|
1240
|
+
getParams: (dbPath) => ({ dbPath }),
|
|
1241
|
+
onProgress: makeProviderProgress("Qoder CN"),
|
|
1242
|
+
});
|
|
1243
|
+
qoderCnResult = {
|
|
1244
|
+
recordsProcessed: result.recordsProcessed || 0,
|
|
1245
|
+
eventsAggregated: result.eventsAggregated || 0,
|
|
1246
|
+
bucketsQueued: result.bucketsQueued || 0,
|
|
1247
|
+
};
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
warnProviderParseFailure("Qoder CN", err, opts);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1162
1254
|
// ── Claude Science (Anthropic's local research workbench, issue #246) ──
|
|
1163
1255
|
// Per-frame token usage lives on the `frames` table of operon-cli.db.
|
|
1164
1256
|
// Passive, incremental, subtract-on-change. There can be more than one DB:
|
|
@@ -2392,6 +2484,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2392
2484
|
antigravityResult.filesProcessed +
|
|
2393
2485
|
opencodeResult.filesProcessed +
|
|
2394
2486
|
qoderResult.recordsProcessed +
|
|
2487
|
+
qoderCnResult.recordsProcessed +
|
|
2395
2488
|
claudeScienceResult.recordsProcessed +
|
|
2396
2489
|
cursorResult.recordsProcessed +
|
|
2397
2490
|
kiroResult.recordsProcessed +
|
|
@@ -2423,6 +2516,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
2423
2516
|
antigravityResult.bucketsQueued +
|
|
2424
2517
|
opencodeResult.bucketsQueued +
|
|
2425
2518
|
qoderResult.bucketsQueued +
|
|
2519
|
+
qoderCnResult.bucketsQueued +
|
|
2426
2520
|
claudeScienceResult.bucketsQueued +
|
|
2427
2521
|
cursorResult.bucketsQueued +
|
|
2428
2522
|
kiroResult.bucketsQueued +
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -79,6 +79,7 @@ async function collectTrackerDiagnostics({
|
|
|
79
79
|
const throttlePath = path.join(trackerDir, "sync.throttle");
|
|
80
80
|
const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
|
|
81
81
|
const autoRetryPath = path.join(trackerDir, "auto.retry.json");
|
|
82
|
+
const syncSkipPath = path.join(trackerDir, "sync.skip.json");
|
|
82
83
|
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
83
84
|
const codeConfigPath = path.join(codeHome, "config.toml");
|
|
84
85
|
const claudeConfigPath = path.join(home, ".claude", "settings.json");
|
|
@@ -96,6 +97,7 @@ async function collectTrackerDiagnostics({
|
|
|
96
97
|
const queueState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
97
98
|
const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
|
|
98
99
|
const autoRetry = await readJson(autoRetryPath);
|
|
100
|
+
const syncSkip = await readJson(syncSkipPath);
|
|
99
101
|
|
|
100
102
|
const queueSize = await safeStatSize(queuePath);
|
|
101
103
|
const offsetBytes = Number(queueState.offset || 0);
|
|
@@ -271,6 +273,14 @@ async function collectTrackerDiagnostics({
|
|
|
271
273
|
}
|
|
272
274
|
: null,
|
|
273
275
|
},
|
|
276
|
+
sync_skip: syncSkip?.at
|
|
277
|
+
? {
|
|
278
|
+
at: syncSkip.at,
|
|
279
|
+
reason: typeof syncSkip.reason === "string" ? syncSkip.reason : null,
|
|
280
|
+
detail: typeof syncSkip.detail === "string" ? syncSkip.detail : null,
|
|
281
|
+
lock_path: redactValue(syncSkip.lockPath, home),
|
|
282
|
+
}
|
|
283
|
+
: null,
|
|
274
284
|
auto_retry: autoRetryAt
|
|
275
285
|
? {
|
|
276
286
|
next_retry_at: autoRetryAt,
|
package/src/lib/fs.js
CHANGED
|
@@ -51,6 +51,7 @@ async function chmod600IfPossible(filePath) {
|
|
|
51
51
|
const LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
52
52
|
const LOCK_HEARTBEAT_MS = 30 * 1000;
|
|
53
53
|
const MAX_RECLAIM_DEPTH = 4;
|
|
54
|
+
const MAX_RECLAIM_SWEEP_DEPTH = 64;
|
|
54
55
|
const TRANSITION_GUARD_RETRY_MS = 5;
|
|
55
56
|
const TRANSITION_GUARD_ATTEMPTS = 1000;
|
|
56
57
|
|
|
@@ -124,6 +125,66 @@ async function existingLockCanBeReclaimed(lockPath) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
async function inspectLock(lockPath) {
|
|
129
|
+
let raw = null;
|
|
130
|
+
try {
|
|
131
|
+
raw = await fs.readFile(lockPath, "utf8");
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (e?.code === "ENOENT") return { exists: false, pid: null, alive: false };
|
|
134
|
+
return { exists: true, pid: null, alive: true };
|
|
135
|
+
}
|
|
136
|
+
const owner = parseLockOwner(raw);
|
|
137
|
+
if (!owner) return { exists: true, pid: null, alive: true };
|
|
138
|
+
return { exists: true, pid: owner.pid, alive: isProcessAlive(owner.pid) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Drop an abandoned lock file. The rename makes the removal atomic, so two
|
|
142
|
+
// sweepers racing on the same debris cannot delete a lease installed in
|
|
143
|
+
// between: only the winner of the rename removes anything.
|
|
144
|
+
async function removeAbandonedLockFile(lockPath) {
|
|
145
|
+
if (!(await existingLockCanBeReclaimed(lockPath))) return false;
|
|
146
|
+
const quarantinePath = `${lockPath}.stale.${process.pid}.${crypto.randomUUID()}`;
|
|
147
|
+
try {
|
|
148
|
+
await fs.rename(lockPath, quarantinePath);
|
|
149
|
+
} catch (_e) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const owner = parseLockOwner(await fs.readFile(quarantinePath, "utf8").catch(() => ""));
|
|
153
|
+
if (owner) {
|
|
154
|
+
await fs.unlink(heartbeatPathFor(lockPath, owner.token)).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
await fs.unlink(quarantinePath).catch(() => {});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A sync killed while it held a transition guard leaves `${lockPath}.reclaim`
|
|
161
|
+
// behind, and the next reclaimer then nests its own guard on top of that
|
|
162
|
+
// orphan. Four nested levels exhaust MAX_RECLAIM_DEPTH, after which every
|
|
163
|
+
// acquisition gives up forever instead of healing (issue #431). Recursion
|
|
164
|
+
// cannot clean the chain — each level needs a guard one level deeper — so sweep
|
|
165
|
+
// it iteratively. Only levels whose owner is dead or whose heartbeat expired are
|
|
166
|
+
// removed, deepest first: dropping a shallow guard while a deeper orphan
|
|
167
|
+
// survives would just rebuild the same clogged chain on the next attempt.
|
|
168
|
+
async function sweepAbandonedReclaimGuards(lockPath) {
|
|
169
|
+
const chain = [];
|
|
170
|
+
let guardPath = lockPath;
|
|
171
|
+
for (let depth = 0; depth < MAX_RECLAIM_SWEEP_DEPTH; depth += 1) {
|
|
172
|
+
guardPath = `${guardPath}.reclaim`;
|
|
173
|
+
try {
|
|
174
|
+
await fs.access(guardPath);
|
|
175
|
+
} catch (_e) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
chain.push(guardPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let removed = 0;
|
|
182
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
183
|
+
if (await removeAbandonedLockFile(chain[index])) removed += 1;
|
|
184
|
+
}
|
|
185
|
+
return removed > 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
127
188
|
function startLockHeartbeat(handle, heartbeatHandle) {
|
|
128
189
|
const beat = async () => {
|
|
129
190
|
try {
|
|
@@ -210,6 +271,7 @@ async function openLock(
|
|
|
210
271
|
beforeReleaseUnlink = null,
|
|
211
272
|
reclaimDepth = 0,
|
|
212
273
|
serializeRelease = true,
|
|
274
|
+
sweptReclaimGuards = false,
|
|
213
275
|
} = {},
|
|
214
276
|
) {
|
|
215
277
|
try {
|
|
@@ -264,7 +326,22 @@ async function openLock(
|
|
|
264
326
|
reclaimDepth: reclaimDepth + 1,
|
|
265
327
|
serializeRelease: false,
|
|
266
328
|
});
|
|
267
|
-
if (!reclaimGuard)
|
|
329
|
+
if (!reclaimGuard) {
|
|
330
|
+
// Either a live reclaimer holds the guard (nothing to do) or the guard
|
|
331
|
+
// chain is clogged with abandoned guards. Sweeping the latter is the
|
|
332
|
+
// only escape, and it is attempted once per acquisition. beforeReclaim
|
|
333
|
+
// already ran, so the retry must not invoke it a second time.
|
|
334
|
+
if (!sweptReclaimGuards && (await sweepAbandonedReclaimGuards(lockPath))) {
|
|
335
|
+
return openLock(lockPath, {
|
|
336
|
+
quietIfLocked,
|
|
337
|
+
beforeReleaseUnlink,
|
|
338
|
+
reclaimDepth,
|
|
339
|
+
serializeRelease,
|
|
340
|
+
sweptReclaimGuards: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
268
345
|
|
|
269
346
|
let quarantinePath = null;
|
|
270
347
|
try {
|
|
@@ -326,4 +403,5 @@ module.exports = {
|
|
|
326
403
|
writeJson,
|
|
327
404
|
chmod600IfPossible,
|
|
328
405
|
openLock,
|
|
406
|
+
inspectLock,
|
|
329
407
|
};
|