tokentracker-cli 0.81.2 → 0.81.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.
Files changed (128) hide show
  1. package/dashboard/dist/assets/AchievementBadge-DiSoAalB.js +1 -0
  2. package/dashboard/dist/assets/AchievementsPage-DI_cvzqy.js +2 -0
  3. package/dashboard/dist/assets/AchievementsSection-ClWZZNvc.js +1 -0
  4. package/dashboard/dist/assets/BadgeDetailModal-DpQeG_6H.js +1 -0
  5. package/dashboard/dist/assets/Card-B0hyofoy.js +1 -0
  6. package/dashboard/dist/assets/ClawdAnimated-DZJGQ3-9.js +1 -0
  7. package/dashboard/dist/assets/CommunityStatsModal-Dcikt0kT.js +1 -0
  8. package/dashboard/dist/assets/ConfirmModal-DCfRP4oI.js +1 -0
  9. package/dashboard/dist/assets/DashboardPage-DBajsWEp.js +1 -0
  10. package/dashboard/dist/assets/DevicePage-De6KuFaJ.js +1 -0
  11. package/dashboard/dist/assets/DialogDescription-uQ4gVIv9.js +1 -0
  12. package/dashboard/dist/assets/DialogTitle-BvElfi0r.js +1 -0
  13. package/dashboard/dist/assets/FadeIn-DZoN7vbQ.js +1 -0
  14. package/dashboard/dist/assets/HeaderGithubStar-C9HQnEsI.js +1 -0
  15. package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-82zniXwO.js} +3 -3
  16. package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-Df9fdR0n.js} +168 -168
  17. package/dashboard/dist/assets/LeaderboardAvatar-CjfEgoR8.js +1 -0
  18. package/dashboard/dist/assets/LeaderboardPage-BdbE6Nhj.js +2 -0
  19. package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-CDTK21dp.js} +4 -4
  20. package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-B6QWnwB1.js} +3 -3
  21. package/dashboard/dist/assets/{LimitsPage-BiQpqxj8.js → LimitsPage-f0L0d15j.js} +4 -4
  22. package/dashboard/dist/assets/LocalOnlyNotice-DOcSDrOy.js +1 -0
  23. package/dashboard/dist/assets/LoginPage-WY_sRA7I.js +1 -0
  24. package/dashboard/dist/assets/PetPage-CNbgNE6f.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-Cv525-dF.js +1 -0
  26. package/dashboard/dist/assets/ResetPasswordPage-vrcj6bBP.js +1 -0
  27. package/dashboard/dist/assets/Select-3HkgkO8y.js +1 -0
  28. package/dashboard/dist/assets/SelectItemText-Bz8FAKEv.js +1 -0
  29. package/dashboard/dist/assets/SettingsPage-CVldAt69.js +1 -0
  30. package/dashboard/dist/assets/SkillsPage-Dnz3eGcE.js +1 -0
  31. package/dashboard/dist/assets/TrendMonitor-OopQ06B8.js +19 -0
  32. package/dashboard/dist/assets/WidgetsPage-4dWjZkc9.js +1 -0
  33. package/dashboard/dist/assets/WrappedPage-Dbd-8EKD.js +1 -0
  34. package/dashboard/dist/assets/__vite-browser-external-CQxW1DuO.css +1 -0
  35. package/dashboard/dist/assets/__vite-browser-external-CxN2E6q5.js +1312 -0
  36. package/dashboard/dist/assets/agent-logos-CT9fumzx.js +1 -0
  37. package/dashboard/dist/assets/arrow-up-right-CGnVz9Km.js +1 -0
  38. package/dashboard/dist/assets/check-CkI3WNti.js +1 -0
  39. package/dashboard/dist/assets/copy-Bh3g2_O-.js +1 -0
  40. package/dashboard/dist/assets/download-BITw7nKX.js +1 -0
  41. package/dashboard/dist/assets/flame-ByAsnNar.js +1 -0
  42. package/dashboard/dist/assets/format-tokens-Cu8JtuN4.js +1 -0
  43. package/dashboard/dist/assets/icons-D9OB0N43.js +1 -0
  44. package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
  45. package/dashboard/dist/assets/info-Mu2CLf-A.js +1 -0
  46. package/dashboard/dist/assets/link-2-CSlVaUw9.js +1 -0
  47. package/dashboard/dist/assets/loader-circle-CYck0YaS.js +1 -0
  48. package/dashboard/dist/assets/provider-display-BB925tPa.js +42 -0
  49. package/dashboard/dist/assets/terminal-4YysOnTe.js +1 -0
  50. package/dashboard/dist/assets/trash-2-C8-ZfrZG.js +1 -0
  51. package/dashboard/dist/assets/use-community-stats-Cxt-otko.js +1 -0
  52. package/dashboard/dist/assets/use-limits-display-prefs-BUFZh6fe.js +1 -0
  53. package/dashboard/dist/assets/use-native-settings-DbdPXxGf.js +1 -0
  54. package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-BZMM-iIj.js} +2 -2
  55. package/dashboard/dist/assets/use-quality-per-dollar-pref-DXoW5A0_.js +1 -0
  56. package/dashboard/dist/assets/use-transform-CuAoUb8H.js +1 -0
  57. package/dashboard/dist/assets/use-usage-limits-BEc3QMzR.js +1 -0
  58. package/dashboard/dist/assets/useCurrency-CpDnFC0N.js +1 -0
  59. package/dashboard/dist/assets/useScrollLock-Vyaif-7C.js +1 -0
  60. package/dashboard/dist/assets/useTokenFormat-B8FrUarf.js +1 -0
  61. package/dashboard/dist/assets/zap-CdeKJ6HL.js +1 -0
  62. package/dashboard/dist/index.html +2 -2
  63. package/dashboard/dist/share.html +2 -2
  64. package/package.json +1 -1
  65. package/src/commands/serve.js +8 -6
  66. package/src/commands/status.js +2 -1
  67. package/src/commands/sync.js +164 -36
  68. package/src/lib/codex-context-breakdown.js +330 -13
  69. package/src/lib/codex-rollout-parser.js +323 -42
  70. package/src/lib/cursor-store.js +1289 -0
  71. package/src/lib/diagnostics.js +8 -5
  72. package/src/lib/fs.js +254 -16
  73. package/src/lib/local-api.js +106 -17
  74. package/src/lib/pricing/index.js +8 -0
  75. package/src/lib/pricing/seed-snapshot.json +1 -1
  76. package/src/lib/rollout.js +101 -9
  77. package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
  78. package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
  79. package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
  80. package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
  81. package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
  82. package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
  83. package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
  84. package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
  85. package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
  86. package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
  87. package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
  88. package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
  89. package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
  90. package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
  91. package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
  92. package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
  93. package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
  94. package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
  95. package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
  96. package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
  97. package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
  98. package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
  99. package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
  100. package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
  101. package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
  102. package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
  103. package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
  104. package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
  105. package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
  106. package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
  107. package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
  108. package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
  109. package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
  110. package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
  111. package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
  112. package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
  113. package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
  114. package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
  115. package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
  116. package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
  117. package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
  118. package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
  119. package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
  120. package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
  121. package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
  122. package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
  123. package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
  124. package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
  125. package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
  126. package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
  127. package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
  128. package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
@@ -0,0 +1,1289 @@
1
+ const crypto = require("node:crypto");
2
+ const fssync = require("node:fs");
3
+ const fs = require("node:fs/promises");
4
+ const path = require("node:path");
5
+
6
+ const {
7
+ ensureDir,
8
+ readJson,
9
+ readJsonStrict,
10
+ writeJson,
11
+ } = require("./fs");
12
+
13
+ const STORE_VERSION = 2;
14
+ const STORE_DIRNAME = "cursor-store-v2";
15
+ const MANIFEST_FILENAME = "manifest.json";
16
+ const GENERATIONS_DIRNAME = "generations";
17
+ const DEFAULT_ACTIVATION_BYTES = 16 * 1024 * 1024;
18
+ const PROJECT_ABSENT_CONTEXT_RESCAN_MS = 24 * 60 * 60 * 1000;
19
+ const CURSOR_STORE_RETRY_CODE = "TOKENTRACKER_CURSOR_STORE_RETRY";
20
+
21
+ async function openCursorStore({
22
+ trackerDir,
23
+ cursorsPath = path.join(trackerDir, "cursors.json"),
24
+ activationBytes = DEFAULT_ACTIVATION_BYTES,
25
+ forceV2 = false,
26
+ codexRoots = [],
27
+ failureInjector = null,
28
+ } = {}) {
29
+ if (typeof trackerDir !== "string" || trackerDir.length === 0) {
30
+ throw new Error("trackerDir is required");
31
+ }
32
+
33
+ const storeRoot = path.join(trackerDir, STORE_DIRNAME);
34
+ const manifestPath = path.join(storeRoot, MANIFEST_FILENAME);
35
+ const normalizedCodexRoots = normalizeCodexRoots(codexRoots);
36
+ const legacyFingerprint = await fingerprintFile(cursorsPath);
37
+ let manifest = await readJson(manifestPath);
38
+
39
+ if (isManifest(manifest)) {
40
+ const legacyChanged = legacyFingerprint
41
+ ? !sameFingerprint(legacyFingerprint, manifest.legacyFingerprint)
42
+ : false;
43
+ if (legacyChanged) {
44
+ manifest = await migrateLegacyCursorState({
45
+ cursorsPath,
46
+ storeRoot,
47
+ legacyFingerprint,
48
+ previousManifest: manifest,
49
+ codexRoots: normalizedCodexRoots,
50
+ failureInjector,
51
+ });
52
+ }
53
+
54
+ const opened = await openManifestGeneration({
55
+ cursorsPath,
56
+ storeRoot,
57
+ manifest,
58
+ codexRoots: normalizedCodexRoots,
59
+ failureInjector,
60
+ });
61
+ if (opened) return opened;
62
+
63
+ manifest = await migrateLegacyCursorState({
64
+ cursorsPath,
65
+ storeRoot,
66
+ legacyFingerprint,
67
+ previousManifest: manifest,
68
+ codexRoots: normalizedCodexRoots,
69
+ failureInjector,
70
+ });
71
+ const recovered = await openManifestGeneration({
72
+ cursorsPath,
73
+ storeRoot,
74
+ manifest,
75
+ codexRoots: normalizedCodexRoots,
76
+ failureInjector,
77
+ });
78
+ if (recovered) return recovered;
79
+ }
80
+
81
+ const legacySize = Number(legacyFingerprint?.size || 0);
82
+ const shouldActivate = forceV2 || (
83
+ Number.isFinite(activationBytes) &&
84
+ activationBytes >= 0 &&
85
+ legacySize >= activationBytes
86
+ );
87
+ if (!shouldActivate) {
88
+ const cursors = (await readJson(cursorsPath)) || defaultCursorState();
89
+ return new LegacyCursorStore({ cursorsPath, cursors });
90
+ }
91
+
92
+ manifest = await migrateLegacyCursorState({
93
+ cursorsPath,
94
+ storeRoot,
95
+ legacyFingerprint,
96
+ previousManifest: null,
97
+ codexRoots: normalizedCodexRoots,
98
+ failureInjector,
99
+ });
100
+ const opened = await openManifestGeneration({
101
+ cursorsPath,
102
+ storeRoot,
103
+ manifest,
104
+ codexRoots: normalizedCodexRoots,
105
+ failureInjector,
106
+ });
107
+ if (!opened) throw new Error("Unable to open migrated cursor store");
108
+ return opened;
109
+ }
110
+
111
+ async function readCursorStateSummary({ trackerDir, cursorsPath } = {}) {
112
+ const resolvedCursorsPath = cursorsPath || path.join(trackerDir, "cursors.json");
113
+ const storeRoot = path.join(path.dirname(resolvedCursorsPath), STORE_DIRNAME);
114
+ const manifest = await readJson(path.join(storeRoot, MANIFEST_FILENAME));
115
+ if (isManifest(manifest)) {
116
+ const legacyFingerprint = await fingerprintFile(resolvedCursorsPath);
117
+ if (
118
+ legacyFingerprint &&
119
+ !sameFingerprint(legacyFingerprint, manifest.legacyFingerprint)
120
+ ) {
121
+ return summarizeLegacyCursorState(
122
+ await readJson(resolvedCursorsPath),
123
+ { legacyDrift: true },
124
+ );
125
+ }
126
+ const opened = await readGeneration({ storeRoot, generationId: manifest.current }) ||
127
+ await readGeneration({ storeRoot, generationId: manifest.previous });
128
+ if (opened) {
129
+ return {
130
+ cursors: opened.core,
131
+ mode: "v2",
132
+ legacyDrift: false,
133
+ fileCount: Number(opened.metadata?.counts?.totalFiles || 0),
134
+ codexFileCount: Number(opened.metadata?.counts?.codexFiles || 0),
135
+ codexEventCount: Number(opened.metadata?.counts?.codexEvents || 0),
136
+ };
137
+ }
138
+ }
139
+ return summarizeLegacyCursorState(await readJson(resolvedCursorsPath));
140
+ }
141
+
142
+ function summarizeLegacyCursorState(cursors, { legacyDrift = false } = {}) {
143
+ return {
144
+ cursors,
145
+ mode: "legacy",
146
+ legacyDrift,
147
+ fileCount:
148
+ cursors?.files && typeof cursors.files === "object"
149
+ ? Object.keys(cursors.files).length
150
+ : null,
151
+ codexFileCount: null,
152
+ codexEventCount: Array.isArray(cursors?.codexHashes)
153
+ ? cursors.codexHashes.length
154
+ : null,
155
+ };
156
+ }
157
+
158
+ class LegacyCursorStore {
159
+ constructor({ cursorsPath, cursors }) {
160
+ this.mode = "legacy";
161
+ this.cursorsPath = cursorsPath;
162
+ this.cursors = cursors;
163
+ this.codexEventStore = null;
164
+ this.fileShardLoadCount = 0;
165
+ }
166
+
167
+ get fileCount() {
168
+ return Object.keys(this.cursors?.files || {}).length;
169
+ }
170
+
171
+ get currentCorePath() {
172
+ return this.cursorsPath;
173
+ }
174
+
175
+ async loadCodexFilesForPaths() {}
176
+
177
+ async materializeAllCodexState() {}
178
+
179
+ async canSkipCodexDay() {
180
+ return false;
181
+ }
182
+
183
+ async commit(cursors = this.cursors) {
184
+ await writeJson(this.cursorsPath, cursors);
185
+ }
186
+ }
187
+
188
+ class V2CursorStore {
189
+ constructor({
190
+ cursorsPath,
191
+ storeRoot,
192
+ manifest,
193
+ generation,
194
+ fallbackGenerationId,
195
+ codexRoots,
196
+ failureInjector,
197
+ }) {
198
+ this.mode = "v2";
199
+ this.cursorsPath = cursorsPath;
200
+ this.storeRoot = storeRoot;
201
+ this.manifest = manifest;
202
+ this.generation = generation.metadata;
203
+ this.generationDir = generation.directory;
204
+ this.fallbackGenerationId = fallbackGenerationId;
205
+ this.codexRoots = codexRoots;
206
+ this.cursors = generation.core;
207
+ this.failureInjector = failureInjector;
208
+ this.loadedFileShards = new Map();
209
+ this.loadedEventSets = new Map();
210
+ this.pendingEventKeys = new Map();
211
+ this.skipValidationCache = new Map();
212
+ this.fileShardLoadCount = 0;
213
+ this.materializedCodexHashes = false;
214
+
215
+ if (!this.cursors.files || typeof this.cursors.files !== "object") {
216
+ this.cursors.files = {};
217
+ }
218
+
219
+ const self = this;
220
+ this.codexEventStore = {
221
+ get size() {
222
+ let pending = 0;
223
+ for (const values of self.pendingEventKeys.values()) pending += values.size;
224
+ return Number(self.generation?.counts?.codexEvents || 0) + pending;
225
+ },
226
+ has(key) {
227
+ return self.hasCodexEvent(key);
228
+ },
229
+ add(key) {
230
+ self.addCodexEvent(key);
231
+ },
232
+ };
233
+ }
234
+
235
+ get fileCount() {
236
+ return Number(this.generation?.counts?.totalFiles || 0);
237
+ }
238
+
239
+ get currentCorePath() {
240
+ return path.join(this.generationDir, this.generation?.coreFile || "core.json");
241
+ }
242
+
243
+ async loadCodexFilesForPaths(paths, cursors = this.cursors) {
244
+ const shardKeys = new Set();
245
+ for (const entry of Array.isArray(paths) ? paths : []) {
246
+ const filePath = typeof entry === "string" ? entry : entry?.path;
247
+ if (!isCodexSessionCursorPathForRoots(filePath, this.codexRoots)) continue;
248
+ shardKeys.add(codexFileShardKey(filePath));
249
+ }
250
+ for (let attempt = 0; attempt < 2; attempt += 1) {
251
+ try {
252
+ for (const shardKey of shardKeys) {
253
+ await this.loadFileShard(shardKey, cursors);
254
+ }
255
+ return { restarted: attempt > 0 };
256
+ } catch (error) {
257
+ if (!isCursorStoreRetry(error) || attempt > 0) throw error;
258
+ }
259
+ }
260
+ return { restarted: false };
261
+ }
262
+
263
+ async loadFileShard(shardKey, cursors = this.cursors) {
264
+ try {
265
+ return await this.loadFileShardUnchecked(shardKey, cursors);
266
+ } catch (error) {
267
+ if (
268
+ error?.code !== "TOKENTRACKER_CURSOR_STORE_CORRUPT" ||
269
+ !this.activateFallbackGeneration(cursors)
270
+ ) {
271
+ throw error;
272
+ }
273
+ throw cursorStoreRetry(error);
274
+ }
275
+ }
276
+
277
+ async loadFileShardUnchecked(shardKey, cursors = this.cursors) {
278
+ if (this.loadedFileShards.has(shardKey)) {
279
+ mergeFileShardIntoCursors(this.loadedFileShards.get(shardKey).data, cursors);
280
+ return this.loadedFileShards.get(shardKey).data;
281
+ }
282
+ const metadata = this.generation?.codexFiles?.[shardKey] || null;
283
+ const filePath = metadata?.file
284
+ ? path.join(this.generationDir, metadata.file)
285
+ : null;
286
+ let data = {};
287
+ if (filePath) {
288
+ let raw;
289
+ try {
290
+ raw = await fs.readFile(filePath, "utf8");
291
+ } catch (error) {
292
+ throw cursorStoreCorruption(
293
+ `Unable to read Codex cursor shard ${metadata.file}`,
294
+ error,
295
+ );
296
+ }
297
+ assertShardIntegrity(raw, metadata, `Codex cursor shard ${metadata.file}`);
298
+ try {
299
+ data = JSON.parse(raw);
300
+ } catch (error) {
301
+ throw cursorStoreCorruption(
302
+ `Invalid Codex cursor shard ${metadata.file}`,
303
+ error,
304
+ );
305
+ }
306
+ }
307
+ const normalized = data && typeof data === "object" && !Array.isArray(data)
308
+ ? data
309
+ : {};
310
+ if (
311
+ filePath &&
312
+ (normalized !== data || Object.keys(normalized).length !== metadata.count)
313
+ ) {
314
+ throw cursorStoreCorruption(`Invalid Codex cursor shard ${metadata.file}`);
315
+ }
316
+ this.loadedFileShards.set(shardKey, {
317
+ data: normalized,
318
+ originalSerialized: JSON.stringify(normalized),
319
+ });
320
+ this.fileShardLoadCount += 1;
321
+ mergeFileShardIntoCursors(normalized, cursors);
322
+ return normalized;
323
+ }
324
+
325
+ async materializeAllCodexState(cursors = this.cursors) {
326
+ for (let attempt = 0; attempt < 2; attempt += 1) {
327
+ try {
328
+ for (const shardKey of Object.keys(this.generation?.codexFiles || {})) {
329
+ await this.loadFileShard(shardKey, cursors);
330
+ }
331
+ const hashes = [];
332
+ for (const shardKey of Object.keys(this.generation?.codexEvents || {})) {
333
+ const values = this.loadEventSet(shardKey, cursors);
334
+ for (const key of values) hashes.push(key);
335
+ }
336
+ for (const values of this.pendingEventKeys.values()) {
337
+ for (const key of values) hashes.push(key);
338
+ }
339
+ cursors.codexHashes = hashes;
340
+ this.materializedCodexHashes = true;
341
+ return { restarted: attempt > 0 };
342
+ } catch (error) {
343
+ if (!isCursorStoreRetry(error) || attempt > 0) throw error;
344
+ }
345
+ }
346
+ return { restarted: false };
347
+ }
348
+
349
+ async canSkipCodexDay({
350
+ filePath,
351
+ dayInventoryCache,
352
+ nowMs = Date.now(),
353
+ } = {}) {
354
+ const shardKey = codexFileShardKey(filePath);
355
+ if (shardKey === "misc") return false;
356
+ const dayDir = codexDayDirectory(filePath);
357
+ if (!dayDir) return false;
358
+ const shardMetadata = this.generation?.codexFiles?.[shardKey];
359
+ const directoryMetadata = shardMetadata?.directories?.[dayDir];
360
+ const inventory = dayInventoryCache?.days?.[dayDir];
361
+ if (
362
+ !directoryMetadata?.complete ||
363
+ !inventory ||
364
+ typeof inventory.statKey !== "string" ||
365
+ inventory.statKey !== directoryMetadata.statKey ||
366
+ !Array.isArray(inventory.files) ||
367
+ inventory.files.length !== directoryMetadata.fileCount
368
+ ) {
369
+ return false;
370
+ }
371
+
372
+ if (this.skipValidationCache.has(shardKey)) {
373
+ return this.skipValidationCache.get(shardKey);
374
+ }
375
+ const valid = await validateProjectSummary(shardMetadata.projectSummary, nowMs);
376
+ this.skipValidationCache.set(shardKey, valid);
377
+ return valid;
378
+ }
379
+
380
+ hasCodexEvent(key) {
381
+ const shardKey = codexEventShardKey(key);
382
+ if (this.pendingEventKeys.get(shardKey)?.has(key)) return true;
383
+ return this.loadEventSet(shardKey).has(key);
384
+ }
385
+
386
+ addCodexEvent(key) {
387
+ if (typeof key !== "string" || key.length === 0) return;
388
+ const shardKey = codexEventShardKey(key);
389
+ const loaded = this.loadedEventSets.get(shardKey);
390
+ if (loaded?.has(key)) return;
391
+ if (!this.pendingEventKeys.has(shardKey)) {
392
+ this.pendingEventKeys.set(shardKey, new Set());
393
+ }
394
+ this.pendingEventKeys.get(shardKey).add(key);
395
+ if (loaded) loaded.add(key);
396
+ }
397
+
398
+ loadEventSet(shardKey, cursors = this.cursors) {
399
+ try {
400
+ return this.loadEventSetUnchecked(shardKey);
401
+ } catch (error) {
402
+ if (
403
+ error?.code !== "TOKENTRACKER_CURSOR_STORE_CORRUPT" ||
404
+ !this.activateFallbackGeneration(cursors)
405
+ ) {
406
+ throw error;
407
+ }
408
+ throw cursorStoreRetry(error);
409
+ }
410
+ }
411
+
412
+ loadEventSetUnchecked(shardKey) {
413
+ if (this.loadedEventSets.has(shardKey)) {
414
+ return this.loadedEventSets.get(shardKey);
415
+ }
416
+ const metadata = this.generation?.codexEvents?.[shardKey] || null;
417
+ const filePath = metadata?.file
418
+ ? path.join(this.generationDir, metadata.file)
419
+ : null;
420
+ let raw = "";
421
+ if (filePath) {
422
+ try {
423
+ raw = fssync.readFileSync(filePath, "utf8");
424
+ } catch (e) {
425
+ throw cursorStoreCorruption(
426
+ `Unable to read Codex event shard ${metadata.file}`,
427
+ e,
428
+ );
429
+ }
430
+ assertShardIntegrity(raw, metadata, `Codex event shard ${metadata.file}`);
431
+ }
432
+ const rows = raw.split("\n").filter(Boolean);
433
+ const values = new Set(rows);
434
+ if (
435
+ filePath &&
436
+ (rows.length !== metadata.count || values.size !== metadata.count)
437
+ ) {
438
+ throw cursorStoreCorruption(`Invalid Codex event shard ${metadata.file}`);
439
+ }
440
+ this.loadedEventSets.set(shardKey, values);
441
+ return values;
442
+ }
443
+
444
+ activateFallbackGeneration(cursors = this.cursors) {
445
+ const generationId = this.fallbackGenerationId;
446
+ this.fallbackGenerationId = null;
447
+ if (!generationId) return false;
448
+
449
+ const fallback = readGenerationSync({
450
+ storeRoot: this.storeRoot,
451
+ generationId,
452
+ });
453
+ if (!fallback) return false;
454
+
455
+ const fallbackCore = cloneJson(fallback.core);
456
+ replaceObjectContents(this.cursors, fallbackCore);
457
+ if (cursors !== this.cursors) {
458
+ replaceObjectContents(cursors, cloneJson(fallback.core));
459
+ }
460
+ this.generation = fallback.metadata;
461
+ this.generationDir = fallback.directory;
462
+ this.loadedFileShards.clear();
463
+ this.loadedEventSets.clear();
464
+ this.pendingEventKeys.clear();
465
+ this.skipValidationCache.clear();
466
+ this.materializedCodexHashes = false;
467
+ return true;
468
+ }
469
+
470
+ async commit(cursors = this.cursors) {
471
+ const generationId = newGenerationId();
472
+ const generationDir = generationDirectory(this.storeRoot, generationId);
473
+ let published = false;
474
+ try {
475
+ await ensureDir(path.join(generationDir, "codex-files"));
476
+ await ensureDir(path.join(generationDir, "codex-events"));
477
+
478
+ const nextMetadata = {
479
+ version: STORE_VERSION,
480
+ id: generationId,
481
+ createdAt: new Date().toISOString(),
482
+ coreFile: "core.json",
483
+ codexFiles: cloneJson(this.generation?.codexFiles || {}),
484
+ codexEvents: cloneJson(this.generation?.codexEvents || {}),
485
+ counts: { nonCodexFiles: 0, codexFiles: 0, totalFiles: 0, codexEvents: 0 },
486
+ };
487
+
488
+ await cloneGenerationFiles({
489
+ fromDirectory: this.generationDir,
490
+ toDirectory: generationDir,
491
+ metadata: nextMetadata,
492
+ });
493
+
494
+ const codexShardKeysPresent = new Set();
495
+ for (const filePath of Object.keys(cursors.files || {})) {
496
+ if (isCodexSessionCursorPathForRoots(filePath, this.codexRoots)) {
497
+ codexShardKeysPresent.add(codexFileShardKey(filePath));
498
+ }
499
+ }
500
+ for (const shardKey of codexShardKeysPresent) {
501
+ if (!this.loadedFileShards.has(shardKey) && nextMetadata.codexFiles[shardKey]) {
502
+ await this.loadFileShardUnchecked(shardKey, cursors);
503
+ }
504
+ }
505
+
506
+ const { coreFiles, codexFilesByShard } = partitionCursorFiles(
507
+ cursors.files,
508
+ this.codexRoots,
509
+ );
510
+ const dirtyFileShards = new Set([
511
+ ...this.loadedFileShards.keys(),
512
+ ...codexFilesByShard.keys(),
513
+ ]);
514
+ for (const shardKey of dirtyFileShards) {
515
+ const data = codexFilesByShard.get(shardKey) || {};
516
+ const relativeFile = path.join("codex-files", `${safeShardFilename(shardKey)}.json`);
517
+ const targetPath = path.join(generationDir, relativeFile);
518
+ await fs.unlink(targetPath).catch(() => {});
519
+ if (Object.keys(data).length === 0) {
520
+ delete nextMetadata.codexFiles[shardKey];
521
+ continue;
522
+ }
523
+ const serialized = `${JSON.stringify(data)}\n`;
524
+ await fs.writeFile(targetPath, serialized, "utf8");
525
+ nextMetadata.codexFiles[shardKey] = buildCodexFileShardMetadata({
526
+ shardKey,
527
+ data,
528
+ relativeFile,
529
+ serialized,
530
+ dayInventoryCache: cursors.codexDayInventoryCache,
531
+ });
532
+ }
533
+
534
+ if (this.materializedCodexHashes || Array.isArray(cursors.codexHashes)) {
535
+ await rebuildEventShards({
536
+ generationDir,
537
+ metadata: nextMetadata,
538
+ hashes: Array.isArray(cursors.codexHashes) ? cursors.codexHashes : [],
539
+ });
540
+ } else {
541
+ for (const [shardKey, pending] of this.pendingEventKeys.entries()) {
542
+ if (pending.size === 0) continue;
543
+ const existing = this.loadEventSetUnchecked(shardKey);
544
+ for (const key of pending) existing.add(key);
545
+ const relativeFile = path.join("codex-events", `${safeShardFilename(shardKey)}.txt`);
546
+ const targetPath = path.join(generationDir, relativeFile);
547
+ await fs.unlink(targetPath).catch(() => {});
548
+ const serialized = serializeEventSet(existing);
549
+ await fs.writeFile(targetPath, serialized, "utf8");
550
+ nextMetadata.codexEvents[shardKey] = buildEventShardMetadata({
551
+ relativeFile,
552
+ values: existing,
553
+ serialized,
554
+ });
555
+ }
556
+ }
557
+
558
+ const core = {
559
+ ...cursors,
560
+ files: coreFiles,
561
+ };
562
+ delete core.codexHashes;
563
+ await fs.writeFile(
564
+ path.join(generationDir, nextMetadata.coreFile),
565
+ `${JSON.stringify(core)}\n`,
566
+ "utf8",
567
+ );
568
+
569
+ nextMetadata.counts = calculateGenerationCounts(nextMetadata, coreFiles);
570
+ await fs.writeFile(
571
+ path.join(generationDir, "generation.json"),
572
+ `${JSON.stringify(nextMetadata, null, 2)}\n`,
573
+ "utf8",
574
+ );
575
+
576
+ await maybeInjectFailure(this.failureInjector, "beforeManifestSwap");
577
+
578
+ const nextManifest = {
579
+ version: STORE_VERSION,
580
+ current: generationId,
581
+ previous: this.generation?.id || null,
582
+ legacyFingerprint: this.manifest?.legacyFingerprint || null,
583
+ updatedAt: new Date().toISOString(),
584
+ };
585
+ await writeJson(path.join(this.storeRoot, MANIFEST_FILENAME), nextManifest);
586
+ published = true;
587
+
588
+ this.manifest = nextManifest;
589
+ this.generation = nextMetadata;
590
+ this.generationDir = generationDir;
591
+ this.cursors = core;
592
+ this.loadedFileShards.clear();
593
+ this.loadedEventSets.clear();
594
+ this.pendingEventKeys.clear();
595
+ this.skipValidationCache.clear();
596
+ this.materializedCodexHashes = false;
597
+ await cleanupGenerations(this.storeRoot, new Set([
598
+ nextManifest.current,
599
+ nextManifest.previous,
600
+ ].filter(Boolean)));
601
+ } catch (error) {
602
+ if (!published) {
603
+ await fs.rm(generationDir, { recursive: true, force: true }).catch(() => {});
604
+ }
605
+ throw error;
606
+ }
607
+ }
608
+ }
609
+
610
+ async function migrateLegacyCursorState({
611
+ cursorsPath,
612
+ storeRoot,
613
+ legacyFingerprint,
614
+ previousManifest,
615
+ codexRoots,
616
+ failureInjector,
617
+ }) {
618
+ const cursors = await readLegacyCursorStateForMigration(cursorsPath);
619
+ const generationId = newGenerationId();
620
+ const generationDir = generationDirectory(storeRoot, generationId);
621
+ await ensureDir(path.join(generationDir, "codex-files"));
622
+ await ensureDir(path.join(generationDir, "codex-events"));
623
+
624
+ const { coreFiles, codexFilesByShard } = partitionCursorFiles(
625
+ cursors.files || {},
626
+ codexRoots,
627
+ );
628
+ const eventShards = partitionEventHashes(cursors.codexHashes || []);
629
+ const core = { ...cursors, files: coreFiles };
630
+ delete core.codexHashes;
631
+
632
+ const metadata = {
633
+ version: STORE_VERSION,
634
+ id: generationId,
635
+ createdAt: new Date().toISOString(),
636
+ coreFile: "core.json",
637
+ codexFiles: {},
638
+ codexEvents: {},
639
+ counts: {},
640
+ };
641
+
642
+ for (const [shardKey, data] of codexFilesByShard.entries()) {
643
+ const relativeFile = path.join("codex-files", `${safeShardFilename(shardKey)}.json`);
644
+ const serialized = `${JSON.stringify(data)}\n`;
645
+ await fs.writeFile(
646
+ path.join(generationDir, relativeFile),
647
+ serialized,
648
+ "utf8",
649
+ );
650
+ metadata.codexFiles[shardKey] = buildCodexFileShardMetadata({
651
+ shardKey,
652
+ data,
653
+ relativeFile,
654
+ serialized,
655
+ dayInventoryCache: cursors.codexDayInventoryCache,
656
+ });
657
+ }
658
+
659
+ for (const [shardKey, values] of eventShards.entries()) {
660
+ const relativeFile = path.join("codex-events", `${safeShardFilename(shardKey)}.txt`);
661
+ const serialized = serializeEventSet(values);
662
+ await fs.writeFile(
663
+ path.join(generationDir, relativeFile),
664
+ serialized,
665
+ "utf8",
666
+ );
667
+ metadata.codexEvents[shardKey] = buildEventShardMetadata({
668
+ relativeFile,
669
+ values,
670
+ serialized,
671
+ });
672
+ }
673
+
674
+ await fs.writeFile(
675
+ path.join(generationDir, metadata.coreFile),
676
+ `${JSON.stringify(core)}\n`,
677
+ "utf8",
678
+ );
679
+ metadata.counts = calculateGenerationCounts(metadata, coreFiles);
680
+ await fs.writeFile(
681
+ path.join(generationDir, "generation.json"),
682
+ `${JSON.stringify(metadata, null, 2)}\n`,
683
+ "utf8",
684
+ );
685
+
686
+ await maybeInjectFailure(failureInjector, "beforeMigrationManifestSwap");
687
+ const manifest = {
688
+ version: STORE_VERSION,
689
+ current: generationId,
690
+ previous: isManifest(previousManifest) ? previousManifest.current : null,
691
+ legacyFingerprint: legacyFingerprint || null,
692
+ updatedAt: new Date().toISOString(),
693
+ };
694
+ await writeJson(path.join(storeRoot, MANIFEST_FILENAME), manifest);
695
+ await cleanupGenerations(storeRoot, new Set([
696
+ manifest.current,
697
+ manifest.previous,
698
+ ].filter(Boolean)));
699
+ return manifest;
700
+ }
701
+
702
+ async function readLegacyCursorStateForMigration(cursorsPath) {
703
+ const result = await readJsonStrict(cursorsPath);
704
+ if (result.status === "missing") return defaultCursorState();
705
+ if (result.status !== "ok") {
706
+ throw cursorStoreCorruption(
707
+ `Unable to read legacy cursor state ${cursorsPath}`,
708
+ result.error,
709
+ );
710
+ }
711
+ if (!isCursorStateRoot(result.value)) {
712
+ throw cursorStoreCorruption(`Invalid legacy cursor state ${cursorsPath}`);
713
+ }
714
+ if (
715
+ result.value.codexHashes !== undefined &&
716
+ !Array.isArray(result.value.codexHashes)
717
+ ) {
718
+ throw cursorStoreCorruption(`Invalid legacy Codex event state ${cursorsPath}`);
719
+ }
720
+ return result.value;
721
+ }
722
+
723
+ async function openManifestGeneration({
724
+ cursorsPath,
725
+ storeRoot,
726
+ manifest,
727
+ codexRoots,
728
+ failureInjector,
729
+ }) {
730
+ const generationIds = Array.from(new Set([
731
+ manifest.current,
732
+ manifest.previous,
733
+ ].filter(Boolean)));
734
+ for (const generationId of generationIds) {
735
+ const generation = await readGeneration({ storeRoot, generationId });
736
+ if (!generation) continue;
737
+ return new V2CursorStore({
738
+ cursorsPath,
739
+ storeRoot,
740
+ manifest,
741
+ generation,
742
+ fallbackGenerationId:
743
+ generationId === manifest.current && manifest.previous !== generationId
744
+ ? manifest.previous
745
+ : null,
746
+ codexRoots,
747
+ failureInjector,
748
+ });
749
+ }
750
+ return null;
751
+ }
752
+
753
+ async function readGeneration({ storeRoot, generationId }) {
754
+ if (typeof generationId !== "string" || generationId.length === 0) return null;
755
+ const directory = generationDirectory(storeRoot, generationId);
756
+ const metadata = await readJson(path.join(directory, "generation.json"));
757
+ if (!isGenerationMetadata(metadata, generationId)) {
758
+ return null;
759
+ }
760
+ const core = await readJson(path.join(directory, metadata.coreFile));
761
+ if (!isCursorStateRoot(core)) return null;
762
+ if (!(await generationReferencesExist(directory, metadata))) return null;
763
+ const counts = calculateGenerationCounts(metadata, core.files);
764
+ if (!sameGenerationCounts(counts, metadata.counts)) return null;
765
+ return { directory, metadata, core };
766
+ }
767
+
768
+ function readGenerationSync({ storeRoot, generationId }) {
769
+ if (typeof generationId !== "string" || generationId.length === 0) return null;
770
+ const directory = generationDirectory(storeRoot, generationId);
771
+ try {
772
+ const metadata = JSON.parse(
773
+ fssync.readFileSync(path.join(directory, "generation.json"), "utf8"),
774
+ );
775
+ if (!isGenerationMetadata(metadata, generationId)) return null;
776
+ const core = JSON.parse(
777
+ fssync.readFileSync(path.join(directory, metadata.coreFile), "utf8"),
778
+ );
779
+ if (!isCursorStateRoot(core)) return null;
780
+ if (!generationReferencesExistSync(directory, metadata)) return null;
781
+ const counts = calculateGenerationCounts(metadata, core.files);
782
+ if (!sameGenerationCounts(counts, metadata.counts)) return null;
783
+ return { directory, metadata, core };
784
+ } catch (_error) {
785
+ return null;
786
+ }
787
+ }
788
+
789
+ function isCursorStateRoot(value) {
790
+ return Boolean(
791
+ value &&
792
+ typeof value === "object" &&
793
+ !Array.isArray(value) &&
794
+ value.files &&
795
+ typeof value.files === "object" &&
796
+ !Array.isArray(value.files)
797
+ );
798
+ }
799
+
800
+ function isGenerationMetadata(metadata, generationId) {
801
+ if (
802
+ !metadata ||
803
+ typeof metadata !== "object" ||
804
+ Array.isArray(metadata) ||
805
+ metadata.version !== STORE_VERSION ||
806
+ metadata.id !== generationId ||
807
+ metadata.coreFile !== "core.json" ||
808
+ !metadata.codexFiles ||
809
+ typeof metadata.codexFiles !== "object" ||
810
+ Array.isArray(metadata.codexFiles) ||
811
+ !metadata.codexEvents ||
812
+ typeof metadata.codexEvents !== "object" ||
813
+ Array.isArray(metadata.codexEvents) ||
814
+ !metadata.counts ||
815
+ typeof metadata.counts !== "object" ||
816
+ Array.isArray(metadata.counts)
817
+ ) {
818
+ return false;
819
+ }
820
+
821
+ return (
822
+ validShardMetadataMap(metadata.codexFiles, "codex-files") &&
823
+ validShardMetadataMap(metadata.codexEvents, "codex-events")
824
+ );
825
+ }
826
+
827
+ function validShardMetadataMap(shards, expectedDirectory) {
828
+ const files = new Set();
829
+ for (const entry of Object.values(shards)) {
830
+ const reference = normalizeShardReference(entry?.file, expectedDirectory);
831
+ const count = Number(entry?.count);
832
+ const bytes = Number(entry?.bytes);
833
+ if (
834
+ !reference ||
835
+ files.has(reference.name) ||
836
+ !Number.isSafeInteger(count) ||
837
+ count < 0 ||
838
+ !Number.isSafeInteger(bytes) ||
839
+ bytes < 0 ||
840
+ typeof entry?.sha256 !== "string" ||
841
+ !/^[a-f0-9]{64}$/.test(entry.sha256)
842
+ ) {
843
+ return false;
844
+ }
845
+ files.add(reference.name);
846
+ }
847
+ return true;
848
+ }
849
+
850
+ async function generationReferencesExist(directory, metadata) {
851
+ for (const [expectedDirectory, shards] of [
852
+ ["codex-files", metadata.codexFiles],
853
+ ["codex-events", metadata.codexEvents],
854
+ ]) {
855
+ const expectedNames = Object.values(shards)
856
+ .map((entry) => normalizeShardReference(entry.file, expectedDirectory).name);
857
+ if (expectedNames.length === 0) continue;
858
+
859
+ let entries;
860
+ try {
861
+ entries = await fs.readdir(path.join(directory, expectedDirectory), {
862
+ withFileTypes: true,
863
+ });
864
+ } catch (_e) {
865
+ return false;
866
+ }
867
+ const files = new Map(entries.map((entry) => [entry.name, entry]));
868
+ if (expectedNames.some((name) => !files.get(name)?.isFile())) return false;
869
+ }
870
+ return true;
871
+ }
872
+
873
+ function generationReferencesExistSync(directory, metadata) {
874
+ for (const [expectedDirectory, shards] of [
875
+ ["codex-files", metadata.codexFiles],
876
+ ["codex-events", metadata.codexEvents],
877
+ ]) {
878
+ const expectedNames = Object.values(shards)
879
+ .map((entry) => normalizeShardReference(entry.file, expectedDirectory).name);
880
+ if (expectedNames.length === 0) continue;
881
+
882
+ let entries;
883
+ try {
884
+ entries = fssync.readdirSync(path.join(directory, expectedDirectory), {
885
+ withFileTypes: true,
886
+ });
887
+ } catch (_error) {
888
+ return false;
889
+ }
890
+ const files = new Map(entries.map((entry) => [entry.name, entry]));
891
+ if (expectedNames.some((name) => !files.get(name)?.isFile())) return false;
892
+ }
893
+ return true;
894
+ }
895
+
896
+ function normalizeShardReference(relativeFile, expectedDirectory) {
897
+ if (typeof relativeFile !== "string" || relativeFile.length === 0) return null;
898
+ const normalized = relativeFile.replace(/\\/g, "/");
899
+ const parts = normalized.split("/");
900
+ if (
901
+ parts.length !== 2 ||
902
+ parts[0] !== expectedDirectory ||
903
+ !parts[1] ||
904
+ parts[1] === "." ||
905
+ parts[1] === ".."
906
+ ) {
907
+ return null;
908
+ }
909
+ return { directory: expectedDirectory, name: parts[1] };
910
+ }
911
+
912
+ function sameGenerationCounts(left, right) {
913
+ return ["nonCodexFiles", "codexFiles", "totalFiles", "codexEvents"]
914
+ .every((key) => Number(left?.[key]) === Number(right?.[key]));
915
+ }
916
+
917
+ async function cloneGenerationFiles({ fromDirectory, toDirectory, metadata }) {
918
+ for (const entry of Object.values(metadata.codexFiles || {})) {
919
+ if (entry?.file) await cloneOrCopy(fromDirectory, toDirectory, entry.file);
920
+ }
921
+ for (const entry of Object.values(metadata.codexEvents || {})) {
922
+ if (entry?.file) await cloneOrCopy(fromDirectory, toDirectory, entry.file);
923
+ }
924
+ }
925
+
926
+ async function cloneOrCopy(fromDirectory, toDirectory, relativeFile) {
927
+ const source = path.join(fromDirectory, relativeFile);
928
+ const target = path.join(toDirectory, relativeFile);
929
+ await ensureDir(path.dirname(target));
930
+ await fs.copyFile(source, target, fssync.constants.COPYFILE_FICLONE);
931
+ }
932
+
933
+ async function rebuildEventShards({ generationDir, metadata, hashes }) {
934
+ for (const entry of Object.values(metadata.codexEvents || {})) {
935
+ if (entry?.file) await fs.unlink(path.join(generationDir, entry.file)).catch(() => {});
936
+ }
937
+ metadata.codexEvents = {};
938
+ for (const [shardKey, values] of partitionEventHashes(hashes).entries()) {
939
+ const relativeFile = path.join("codex-events", `${safeShardFilename(shardKey)}.txt`);
940
+ const serialized = serializeEventSet(values);
941
+ await fs.writeFile(
942
+ path.join(generationDir, relativeFile),
943
+ serialized,
944
+ "utf8",
945
+ );
946
+ metadata.codexEvents[shardKey] = buildEventShardMetadata({
947
+ relativeFile,
948
+ values,
949
+ serialized,
950
+ });
951
+ }
952
+ }
953
+
954
+ function buildCodexFileShardMetadata({
955
+ shardKey,
956
+ data,
957
+ relativeFile,
958
+ serialized,
959
+ dayInventoryCache,
960
+ }) {
961
+ const directories = {};
962
+ const configs = new Map();
963
+ let absentFreshUntil = null;
964
+
965
+ for (const [filePath, cursor] of Object.entries(data || {})) {
966
+ const dayDir = codexDayDirectory(filePath);
967
+ if (dayDir) {
968
+ if (!directories[dayDir]) directories[dayDir] = { paths: [] };
969
+ directories[dayDir].paths.push(filePath);
970
+ }
971
+ const context = cursor?.projectFileContext;
972
+ if (context?.absent === true) {
973
+ const checkedAtMs = Number(context.checkedAtMs);
974
+ if (Number.isFinite(checkedAtMs) && checkedAtMs > 0) {
975
+ const expiresAt = checkedAtMs + PROJECT_ABSENT_CONTEXT_RESCAN_MS;
976
+ absentFreshUntil = absentFreshUntil == null
977
+ ? expiresAt
978
+ : Math.min(absentFreshUntil, expiresAt);
979
+ } else {
980
+ absentFreshUntil = 0;
981
+ }
982
+ continue;
983
+ }
984
+ for (const config of projectConfigs(context)) {
985
+ configs.set(config.configPath, config);
986
+ }
987
+ }
988
+
989
+ for (const [dayDir, value] of Object.entries(directories)) {
990
+ const inventory = dayInventoryCache?.days?.[dayDir];
991
+ const inventoryFiles = Array.isArray(inventory?.files) ? inventory.files : [];
992
+ const complete = Boolean(
993
+ typeof inventory?.statKey === "string" &&
994
+ inventoryFiles.length > 0 &&
995
+ inventoryFiles.every((name) => {
996
+ const filePath = path.join(dayDir, name);
997
+ const cursor = data[filePath];
998
+ const offset = Number(cursor?.offset);
999
+ const projectOffset = Number(cursor?.projectOffset);
1000
+ return (
1001
+ Number.isFinite(offset) && offset > 0 &&
1002
+ Number.isFinite(projectOffset) && projectOffset >= offset &&
1003
+ cursor?.projectFileContext && typeof cursor.projectFileContext === "object"
1004
+ );
1005
+ })
1006
+ );
1007
+ directories[dayDir] = {
1008
+ statKey: typeof inventory?.statKey === "string" ? inventory.statKey : null,
1009
+ fileCount: inventoryFiles.length,
1010
+ complete,
1011
+ };
1012
+ }
1013
+
1014
+ return {
1015
+ file: relativeFile,
1016
+ shardKey,
1017
+ count: Object.keys(data || {}).length,
1018
+ ...buildShardIntegrity(serialized),
1019
+ directories,
1020
+ projectSummary: {
1021
+ configs: Array.from(configs.values()),
1022
+ absentFreshUntil,
1023
+ },
1024
+ };
1025
+ }
1026
+
1027
+ function buildEventShardMetadata({ relativeFile, values, serialized }) {
1028
+ return {
1029
+ file: relativeFile,
1030
+ count: values.size,
1031
+ ...buildShardIntegrity(serialized),
1032
+ };
1033
+ }
1034
+
1035
+ function buildShardIntegrity(serialized) {
1036
+ return {
1037
+ bytes: Buffer.byteLength(serialized),
1038
+ sha256: crypto.createHash("sha256").update(serialized).digest("hex"),
1039
+ };
1040
+ }
1041
+
1042
+ function assertShardIntegrity(raw, metadata, label) {
1043
+ const integrity = buildShardIntegrity(raw);
1044
+ if (
1045
+ integrity.bytes !== metadata?.bytes ||
1046
+ integrity.sha256 !== metadata?.sha256
1047
+ ) {
1048
+ throw cursorStoreCorruption(`Integrity check failed for ${label}`);
1049
+ }
1050
+ }
1051
+
1052
+ async function validateProjectSummary(summary, nowMs) {
1053
+ if (!summary || typeof summary !== "object") return false;
1054
+ const absentFreshUntil = Number(summary.absentFreshUntil);
1055
+ if (Number.isFinite(absentFreshUntil) && absentFreshUntil > 0 && nowMs >= absentFreshUntil) {
1056
+ return false;
1057
+ }
1058
+ if (summary.absentFreshUntil === 0) return false;
1059
+ for (const config of Array.isArray(summary.configs) ? summary.configs : []) {
1060
+ const configPath = typeof config?.configPath === "string" ? config.configPath : null;
1061
+ if (!configPath) return false;
1062
+ const stat = await fs.stat(configPath).catch(() => null);
1063
+ if (
1064
+ !stat?.isFile() ||
1065
+ stat.mtimeMs !== config.configMtimeMs ||
1066
+ stat.size !== config.configSize
1067
+ ) {
1068
+ return false;
1069
+ }
1070
+ }
1071
+ return true;
1072
+ }
1073
+
1074
+ function projectConfigs(context) {
1075
+ if (!context || typeof context !== "object") return [];
1076
+ const raw = Array.isArray(context.configs) ? context.configs : [context];
1077
+ const out = [];
1078
+ for (const config of raw) {
1079
+ const configPath = typeof config?.configPath === "string" ? config.configPath : null;
1080
+ if (!configPath) continue;
1081
+ out.push({
1082
+ configPath,
1083
+ configMtimeMs: Number.isFinite(config.configMtimeMs) ? config.configMtimeMs : null,
1084
+ configSize: Number.isFinite(config.configSize) ? config.configSize : null,
1085
+ });
1086
+ }
1087
+ return out;
1088
+ }
1089
+
1090
+ function partitionCursorFiles(files, codexRoots = []) {
1091
+ const coreFiles = {};
1092
+ const codexFilesByShard = new Map();
1093
+ for (const [filePath, cursor] of Object.entries(files || {})) {
1094
+ if (!isCodexSessionCursorPathForRoots(filePath, codexRoots)) {
1095
+ coreFiles[filePath] = cursor;
1096
+ continue;
1097
+ }
1098
+ const shardKey = codexFileShardKey(filePath);
1099
+ if (!codexFilesByShard.has(shardKey)) codexFilesByShard.set(shardKey, {});
1100
+ codexFilesByShard.get(shardKey)[filePath] = cursor;
1101
+ }
1102
+ return { coreFiles, codexFilesByShard };
1103
+ }
1104
+
1105
+ function partitionEventHashes(hashes) {
1106
+ const shards = new Map();
1107
+ for (const key of Array.isArray(hashes) ? hashes : []) {
1108
+ if (typeof key !== "string" || key.length === 0) continue;
1109
+ const shardKey = codexEventShardKey(key);
1110
+ if (!shards.has(shardKey)) shards.set(shardKey, new Set());
1111
+ shards.get(shardKey).add(key);
1112
+ }
1113
+ return shards;
1114
+ }
1115
+
1116
+ function mergeFileShardIntoCursors(data, cursors) {
1117
+ if (!cursors.files || typeof cursors.files !== "object") cursors.files = {};
1118
+ for (const [filePath, cursor] of Object.entries(data || {})) {
1119
+ if (!(filePath in cursors.files)) cursors.files[filePath] = cursor;
1120
+ }
1121
+ }
1122
+
1123
+ function calculateGenerationCounts(metadata, coreFiles) {
1124
+ const nonCodexFiles = Object.keys(coreFiles || {}).length;
1125
+ const codexFiles = Object.values(metadata.codexFiles || {})
1126
+ .reduce((sum, entry) => sum + Number(entry?.count || 0), 0);
1127
+ const codexEvents = Object.values(metadata.codexEvents || {})
1128
+ .reduce((sum, entry) => sum + Number(entry?.count || 0), 0);
1129
+ return {
1130
+ nonCodexFiles,
1131
+ codexFiles,
1132
+ totalFiles: nonCodexFiles + codexFiles,
1133
+ codexEvents,
1134
+ };
1135
+ }
1136
+
1137
+ function isCodexSessionCursorPath(filePath, codexRoots = []) {
1138
+ return isCodexSessionCursorPathForRoots(
1139
+ filePath,
1140
+ normalizeCodexRoots(codexRoots),
1141
+ );
1142
+ }
1143
+
1144
+ function isCodexSessionCursorPathForRoots(filePath, codexRoots) {
1145
+ if (typeof filePath !== "string") return false;
1146
+ const normalized = filePath.replace(/\\/g, "/");
1147
+ if (/\/\.codex\/(?:archived_)?sessions\//.test(normalized)) return true;
1148
+ return codexRoots.some((root) => (
1149
+ normalized.startsWith(`${root}/sessions/`) ||
1150
+ normalized.startsWith(`${root}/archived_sessions/`)
1151
+ ));
1152
+ }
1153
+
1154
+ function normalizeCodexRoots(roots) {
1155
+ const values = Array.isArray(roots) ? roots : [roots];
1156
+ return Array.from(new Set(values
1157
+ .filter((root) => typeof root === "string" && root.trim().length > 0)
1158
+ .map((root) => root.replace(/\\/g, "/").replace(/\/+$/, ""))));
1159
+ }
1160
+
1161
+ function codexFileShardKey(filePath) {
1162
+ if (typeof filePath !== "string") return "misc";
1163
+ const normalized = filePath.replace(/\\/g, "/");
1164
+ const match = normalized.match(/\/(\d{4})\/(\d{2})\/(\d{2})\//);
1165
+ return match ? `${match[1]}-${match[2]}-${match[3]}` : "misc";
1166
+ }
1167
+
1168
+ function codexDayDirectory(filePath) {
1169
+ if (codexFileShardKey(filePath) === "misc") return null;
1170
+ return path.dirname(filePath);
1171
+ }
1172
+
1173
+ function codexEventShardKey(key) {
1174
+ if (typeof key !== "string") return "misc";
1175
+ const separator = key.indexOf(":");
1176
+ const day = separator >= 0 ? key.slice(separator + 1, separator + 11) : "";
1177
+ return /^\d{4}-\d{2}-\d{2}$/.test(day) ? day : "misc";
1178
+ }
1179
+
1180
+ function safeShardFilename(shardKey) {
1181
+ return /^\d{4}-\d{2}-\d{2}$/.test(shardKey) ? shardKey : "misc";
1182
+ }
1183
+
1184
+ function serializeEventSet(values) {
1185
+ const rows = Array.from(values || []);
1186
+ return rows.length > 0 ? `${rows.join("\n")}\n` : "";
1187
+ }
1188
+
1189
+ function defaultCursorState() {
1190
+ return { version: 1, files: {}, updatedAt: null };
1191
+ }
1192
+
1193
+ function newGenerationId() {
1194
+ return `gen-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
1195
+ }
1196
+
1197
+ function generationDirectory(storeRoot, generationId) {
1198
+ return path.join(storeRoot, GENERATIONS_DIRNAME, generationId);
1199
+ }
1200
+
1201
+ function isManifest(value) {
1202
+ return Boolean(
1203
+ value &&
1204
+ value.version === STORE_VERSION &&
1205
+ typeof value.current === "string" &&
1206
+ value.current.length > 0
1207
+ );
1208
+ }
1209
+
1210
+ async function fingerprintFile(filePath) {
1211
+ try {
1212
+ const stat = await fs.stat(filePath, { bigint: true });
1213
+ return {
1214
+ dev: String(stat.dev),
1215
+ ino: String(stat.ino),
1216
+ size: String(stat.size),
1217
+ mtimeNs: String(stat.mtimeNs),
1218
+ ctimeNs: String(stat.ctimeNs),
1219
+ };
1220
+ } catch (e) {
1221
+ if (e?.code === "ENOENT") return null;
1222
+ throw e;
1223
+ }
1224
+ }
1225
+
1226
+ function sameFingerprint(left, right) {
1227
+ if (!left || !right) return left === right;
1228
+ return ["dev", "ino", "size", "mtimeNs", "ctimeNs"]
1229
+ .every((key) => String(left[key]) === String(right[key]));
1230
+ }
1231
+
1232
+ async function cleanupGenerations(storeRoot, keep) {
1233
+ const root = path.join(storeRoot, GENERATIONS_DIRNAME);
1234
+ let entries = [];
1235
+ try {
1236
+ entries = await fs.readdir(root, { withFileTypes: true });
1237
+ } catch (e) {
1238
+ if (e?.code !== "ENOENT") throw e;
1239
+ return;
1240
+ }
1241
+ for (const entry of entries) {
1242
+ if (!entry.isDirectory() || keep.has(entry.name)) continue;
1243
+ await fs.rm(path.join(root, entry.name), { recursive: true, force: true });
1244
+ }
1245
+ }
1246
+
1247
+ function cloneJson(value) {
1248
+ return JSON.parse(JSON.stringify(value));
1249
+ }
1250
+
1251
+ function replaceObjectContents(target, source) {
1252
+ for (const key of Object.keys(target)) delete target[key];
1253
+ Object.assign(target, source);
1254
+ }
1255
+
1256
+ function cursorStoreCorruption(message, cause = null) {
1257
+ const error = new Error(message, cause ? { cause } : undefined);
1258
+ error.code = "TOKENTRACKER_CURSOR_STORE_CORRUPT";
1259
+ return error;
1260
+ }
1261
+
1262
+ function cursorStoreRetry(cause) {
1263
+ const error = new Error(
1264
+ "Cursor store generation changed; restart the operation",
1265
+ cause ? { cause } : undefined,
1266
+ );
1267
+ error.code = CURSOR_STORE_RETRY_CODE;
1268
+ return error;
1269
+ }
1270
+
1271
+ function isCursorStoreRetry(error) {
1272
+ return error?.code === CURSOR_STORE_RETRY_CODE;
1273
+ }
1274
+
1275
+ async function maybeInjectFailure(injector, stage) {
1276
+ if (typeof injector === "function") await injector(stage);
1277
+ }
1278
+
1279
+ module.exports = {
1280
+ DEFAULT_ACTIVATION_BYTES,
1281
+ STORE_DIRNAME,
1282
+ STORE_VERSION,
1283
+ codexEventShardKey,
1284
+ codexFileShardKey,
1285
+ isCodexSessionCursorPath,
1286
+ isCursorStoreRetry,
1287
+ openCursorStore,
1288
+ readCursorStateSummary,
1289
+ };