exe-watcher 0.2.67 → 0.2.69

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +195 -85
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,7 +31,7 @@ npm install -g exe-watcher
31
31
  exe-watcher report
32
32
  ```
33
33
 
34
- Version 0.2.67 preserves recorded API-equivalent costs and keeps saved All totals available through refresh failures, restarts and day changes. AI time and the floating quota display are included. Terminal installs require Node 22.13 or newer; the Mac app includes its own compatible runtime. Download the matching app from [Releases](https://github.com/AskExe/exe-watcher/releases/latest) or [odd.community/watcher](https://odd.community/watcher). To run the current CLI from source:
34
+ Version 0.2.69 explains that first-time history setup can take 10 minutes or more for large AI histories, with saved progress and recovery details. Large Claude history scans resume within the existing resource limits. Recorded API-equivalent costs and saved totals are preserved through refresh failures, restarts and day changes. AI time and the floating quota display are included. Terminal installs require Node 22.13 or newer; the Mac app includes its own compatible runtime. Download the matching app from [Releases](https://github.com/AskExe/exe-watcher/releases/latest) or [odd.community/watcher](https://odd.community/watcher). To run the current CLI from source:
35
35
 
36
36
  ```bash
37
37
  git clone https://github.com/AskExe/exe-watcher.git
package/dist/cli.js CHANGED
@@ -2870,6 +2870,43 @@ import { promisify } from "util";
2870
2870
  import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
2871
2871
  import { mkdir as mkdir8, readFile as readFile7, stat as stat5, writeFile as writeFile3, rename as rename6, unlink as unlink5, readdir as readdir3 } from "fs/promises";
2872
2872
  import { join as join10 } from "path";
2873
+ function encodeEntry(value) {
2874
+ let bytes = 0;
2875
+ try {
2876
+ return JSON.stringify(value, (key2, item) => {
2877
+ bytes += key2 ? Buffer.byteLength(JSON.stringify(key2)) + 2 : 0;
2878
+ bytes += item && typeof item === "object" ? 2 : Buffer.byteLength(JSON.stringify(item) ?? "null");
2879
+ if (bytes > MAX_ENTRY) throw new Error("parsed entry limit");
2880
+ return item;
2881
+ });
2882
+ } catch {
2883
+ return void 0;
2884
+ }
2885
+ }
2886
+ function fileProgress(path, dir, cachePath, before) {
2887
+ let startingOffset = 0;
2888
+ return {
2889
+ async load(valid) {
2890
+ if (!before) return;
2891
+ try {
2892
+ const size = (await stat5(cachePath)).size;
2893
+ if (size > MAX_ENTRY) return;
2894
+ chargeRead(size);
2895
+ const saved = JSON.parse((await decompress(await readFile7(cachePath), { maxOutputLength: MAX_ENTRY })).toString("utf8"));
2896
+ if (saved.v !== VERSION2 || saved.fingerprint !== before || !Number.isSafeInteger(saved.offset) || saved.offset <= 0 || saved.offset > Number(before.split(":")[2]) || !valid(saved.state)) return;
2897
+ startingOffset = saved.offset;
2898
+ return { offset: saved.offset, state: saved.state };
2899
+ } catch (error) {
2900
+ if (error instanceof ResourceBudgetError) throw error;
2901
+ }
2902
+ },
2903
+ async save(state, offset) {
2904
+ if (!before || offset <= startingOffset || offset > Number(before.split(":")[2]) || await fingerprint(path) !== before) return;
2905
+ const payload = encodeEntry({ v: VERSION2, fingerprint: before, offset, state });
2906
+ if (payload) await storeParsedEntry(dir, cachePath, payload);
2907
+ }
2908
+ };
2909
+ }
2873
2910
  async function fingerprint(path) {
2874
2911
  try {
2875
2912
  const s = await stat5(path);
@@ -2883,6 +2920,7 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
2883
2920
  const dir = join10(getCacheDir(), "parsed-files");
2884
2921
  const key2 = createHash3("sha256").update(`${VERSION2}:${kind}:${path}`).digest("hex");
2885
2922
  const cachePath = join10(dir, `v${VERSION2}-${key2}.json.gz`);
2923
+ const progressPath = join10(dir, `v${VERSION2}-progress-${key2}.json.gz`);
2886
2924
  const legacyKey = createHash3("sha256").update(`3:${kind}:${path}:${getPricingFingerprint()}`).digest("hex");
2887
2925
  const legacyPath = join10(dir, `v3-${legacyKey}.json.gz`);
2888
2926
  const fingerprintPath = kind === "opencode" ? path.slice(0, path.lastIndexOf(":")) : path;
@@ -2929,11 +2967,11 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
2929
2967
  }
2930
2968
  }
2931
2969
  }
2932
- const data = migrated ?? await parse();
2970
+ const data = migrated ?? await parse(fileProgress(fingerprintPath, dir, progressPath, before));
2933
2971
  const hasUnpricedUsage = lockRecordedCosts(data);
2934
2972
  if (!before || await fingerprint(fingerprintPath) !== before) return data;
2935
2973
  await rememberRange(data);
2936
- const payload = JSON.stringify({
2974
+ const payload = encodeEntry({
2937
2975
  v: VERSION2,
2938
2976
  fingerprint: before,
2939
2977
  data,
@@ -2942,7 +2980,10 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
2942
2980
  hasUnpricedUsage,
2943
2981
  pricingFingerprint: getPricingFingerprint()
2944
2982
  });
2945
- if (Buffer.byteLength(payload) > MAX_ENTRY) return data;
2983
+ if (payload) await storeParsedEntry(dir, cachePath, payload, progressPath);
2984
+ return data;
2985
+ }
2986
+ async function storeParsedEntry(dir, cachePath, payload, obsolete) {
2946
2987
  await mkdir8(dir, { recursive: true });
2947
2988
  const encoded = await compress(Buffer.from(payload));
2948
2989
  const release = await acquireFileLock(join10(dir, "writes.lock"));
@@ -2985,7 +3026,7 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
2985
3026
  if (!(await readFile7(cachePath).catch(() => Buffer.alloc(0))).equals(encoded)) {
2986
3027
  const oldSize = (await stat5(cachePath).catch(() => null))?.size ?? 0;
2987
3028
  let nextSize = total + encoded.length;
2988
- if (!Number.isFinite(nextSize)) return data;
3029
+ if (!Number.isFinite(nextSize)) return;
2989
3030
  if (nextSize > MAX_DISK) {
2990
3031
  const rows2 = [];
2991
3032
  for (const name of await readdir3(dir)) {
@@ -3001,25 +3042,33 @@ async function cachedFileParse(path, kind, parse, warnings, range) {
3001
3042
  });
3002
3043
  nextSize -= row.size;
3003
3044
  }
3004
- if (nextSize > MAX_DISK) return data;
3045
+ if (nextSize > MAX_DISK) return;
3005
3046
  }
3006
3047
  const tmp = `${cachePath}.${randomUUID2()}.tmp`;
3007
3048
  try {
3008
3049
  await writeQuota(nextSize);
3009
3050
  await writeFile3(tmp, encoded, { mode: 384 });
3010
3051
  await rename6(tmp, cachePath);
3052
+ mutationCount++;
3011
3053
  if (oldSize) await writeQuota(nextSize - oldSize);
3054
+ total = nextSize - oldSize;
3012
3055
  } finally {
3013
3056
  await unlink5(tmp).catch(() => {
3014
3057
  });
3015
3058
  }
3016
3059
  }
3060
+ if (obsolete) {
3061
+ const size = (await stat5(obsolete).catch(() => null))?.size ?? 0;
3062
+ if (size) {
3063
+ await unlink5(obsolete);
3064
+ await writeQuota(Math.max(0, total - size));
3065
+ }
3066
+ }
3017
3067
  } finally {
3018
3068
  await release();
3019
3069
  }
3020
- return data;
3021
3070
  }
3022
- var VERSION2, compress, decompress, MAX_ENTRY, MAX_DISK;
3071
+ var VERSION2, compress, decompress, MAX_ENTRY, MAX_DISK, mutationCount, parsedFileProgress;
3023
3072
  var init_parsed_file_cache = __esm({
3024
3073
  "src/parsed-file-cache.ts"() {
3025
3074
  "use strict";
@@ -3034,6 +3083,8 @@ var init_parsed_file_cache = __esm({
3034
3083
  decompress = promisify(gunzip);
3035
3084
  MAX_ENTRY = 16 * 1024 * 1024;
3036
3085
  MAX_DISK = 256 * 1024 * 1024;
3086
+ mutationCount = 0;
3087
+ parsedFileProgress = () => mutationCount;
3037
3088
  }
3038
3089
  });
3039
3090
 
@@ -3142,23 +3193,22 @@ function readSessionFileSync(filePath) {
3142
3193
  closeSync(fd);
3143
3194
  }
3144
3195
  }
3145
- async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord) {
3196
+ async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord, completedLine) {
3146
3197
  const handle = await open5(filePath, "r").catch(() => null);
3147
3198
  if (!handle) return;
3148
3199
  try {
3149
3200
  const size = (await handle.stat()).size;
3150
3201
  if (size <= startByte) return;
3151
- chargeRead(size - startByte);
3152
- const stream = handle.createReadStream({ encoding: "utf-8", start: startByte, end: size - 1, autoClose: false });
3202
+ const stream = handle.createReadStream({ start: startByte, end: size - 1, autoClose: false });
3153
3203
  let pieces = [];
3154
3204
  let lineBytes = 0;
3155
3205
  let discarding = false;
3156
3206
  const maxLineBytes = 32 * 1024 * 1024;
3157
3207
  const append = (piece) => {
3158
3208
  if (discarding) return;
3159
- lineBytes += Buffer.byteLength(piece);
3209
+ lineBytes += piece.length;
3160
3210
  if (lineBytes > maxLineBytes) {
3161
- const prefix = (pieces.slice(0, 4).join("") + piece.slice(0, 1024)).slice(0, 1024);
3211
+ const prefix = Buffer.concat([...pieces.slice(0, 4), piece.subarray(0, 1024)]).toString("utf8").slice(0, 1024);
3162
3212
  if (!skipOversizedRecord?.(prefix)) throw new ResourceBudgetError(`32 MiB JSONL record budget in ${filePath}`);
3163
3213
  discarding = true;
3164
3214
  pieces = [];
@@ -3166,23 +3216,31 @@ async function* readSessionLines(filePath, startByte = 0, skipOversizedRecord) {
3166
3216
  }
3167
3217
  pieces.push(piece);
3168
3218
  };
3219
+ let readThrough = startByte;
3169
3220
  try {
3170
3221
  for await (const chunk of stream) {
3222
+ chargeRead(chunk.length);
3223
+ const chunkStart = readThrough;
3224
+ readThrough += chunk.length;
3171
3225
  let start = 0;
3172
3226
  let newline;
3173
- while ((newline = chunk.indexOf("\n", start)) >= 0) {
3174
- append(chunk.slice(start, newline));
3227
+ while ((newline = chunk.indexOf(10, start)) >= 0) {
3228
+ append(chunk.subarray(start, newline));
3175
3229
  chargeRead(0);
3176
- const line = discarding ? null : pieces.join("").replace(/\r$/, "");
3230
+ const line = discarding ? null : (pieces.length === 1 ? pieces[0] : Buffer.concat(pieces)).toString("utf8").replace(/\r$/, "");
3177
3231
  pieces = [];
3178
3232
  lineBytes = 0;
3179
3233
  discarding = false;
3180
3234
  if (line !== null) yield line;
3235
+ completedLine?.(chunkStart + newline + 1);
3181
3236
  start = newline + 1;
3182
3237
  }
3183
- if (start < chunk.length) append(chunk.slice(start));
3238
+ if (start < chunk.length) append(chunk.subarray(start));
3239
+ }
3240
+ if (pieces.length) {
3241
+ yield (pieces.length === 1 ? pieces[0] : Buffer.concat(pieces)).toString("utf8").replace(/\r$/, "");
3242
+ completedLine?.(readThrough);
3184
3243
  }
3185
- if (pieces.length) yield pieces.join("").replace(/\r$/, "");
3186
3244
  } finally {
3187
3245
  stream.destroy();
3188
3246
  }
@@ -5466,71 +5524,95 @@ function parseApiCall(entry) {
5466
5524
  deduplicationKey: msg.id ?? `claude:${entry.timestamp}`
5467
5525
  };
5468
5526
  }
5469
- async function parseTurns(filePath) {
5527
+ function validTurnScan(value) {
5528
+ const x = value;
5529
+ const amount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
5530
+ const calls = (list) => Array.isArray(list) && list.every((c) => c && typeof c.deduplicationKey === "string" && typeof c.model === "string" && typeof c.timestamp === "string" && amount(c.costUSD) && c.usage && ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"].every((key2) => amount(c.usage[key2])) && Array.isArray(c.tools) && Array.isArray(c.bashCommands));
5531
+ return !!x && Array.isArray(x.turns) && x.turns.every((t) => t && calls(t.assistantCalls) && typeof t.userMessage === "string" && typeof t.timestamp === "string" && typeof t.sessionId === "string") && calls(x.currentCalls) && typeof x.currentUserMessage === "string" && Number.isSafeInteger(x.currentUserSignals) && x.currentUserSignals >= 0 && typeof x.currentTimestamp === "string" && typeof x.currentSessionId === "string" && Number.isSafeInteger(x.lineNumber) && x.lineNumber >= 0;
5532
+ }
5533
+ async function parseTurns(filePath, progress) {
5534
+ const checkpoint = await progress.load(validTurnScan);
5535
+ const saved = checkpoint?.state;
5470
5536
  const seenCalls = /* @__PURE__ */ new Map();
5471
- const turns = [];
5472
- let currentUserMessage = "";
5473
- let currentUserSignals = 0;
5474
- let currentCalls = [];
5475
- let currentTimestamp = "";
5476
- let currentSessionId = "";
5477
- let lineNumber = 0;
5478
- for await (const line of readSessionLines(filePath)) {
5479
- lineNumber++;
5480
- const entry = parseJsonlLine(line);
5481
- if (!entry) continue;
5482
- if (entry.type === "user") {
5483
- const text = getUserMessageText(entry);
5484
- if (text.trim()) {
5485
- if (currentCalls.length > 0) {
5486
- turns.push({
5487
- userMessage: currentUserMessage,
5488
- userMessageSignals: currentUserSignals,
5489
- assistantCalls: currentCalls,
5490
- timestamp: currentTimestamp,
5491
- sessionId: currentSessionId
5492
- });
5537
+ const turns = saved?.turns ?? [];
5538
+ let currentUserMessage = saved?.currentUserMessage ?? "";
5539
+ let currentUserSignals = saved?.currentUserSignals ?? 0;
5540
+ let currentCalls = saved?.currentCalls ?? [];
5541
+ let currentTimestamp = saved?.currentTimestamp ?? "";
5542
+ let currentSessionId = saved?.currentSessionId ?? "";
5543
+ for (const turn of turns) for (const call of turn.assistantCalls) seenCalls.set(call.deduplicationKey, call);
5544
+ for (const call of currentCalls) seenCalls.set(call.deduplicationKey, call);
5545
+ let lineNumber = saved?.lineNumber ?? 0;
5546
+ let offset = checkpoint?.offset ?? 0;
5547
+ try {
5548
+ for await (const line of readSessionLines(filePath, offset, void 0, (next) => {
5549
+ offset = next;
5550
+ })) {
5551
+ chargeRecord();
5552
+ lineNumber++;
5553
+ const entry = parseJsonlLine(line);
5554
+ if (!entry) continue;
5555
+ if (entry.type === "user") {
5556
+ const text = getUserMessageText(entry);
5557
+ if (text.trim()) {
5558
+ if (currentCalls.length > 0) {
5559
+ turns.push({
5560
+ userMessage: currentUserMessage,
5561
+ userMessageSignals: currentUserSignals,
5562
+ assistantCalls: currentCalls,
5563
+ timestamp: currentTimestamp,
5564
+ sessionId: currentSessionId
5565
+ });
5566
+ }
5567
+ const compact = compactUserMessage(text);
5568
+ currentUserMessage = compact.userMessage;
5569
+ currentUserSignals = compact.userMessageSignals;
5570
+ currentCalls = [];
5571
+ currentTimestamp = entry.timestamp ?? "";
5572
+ currentSessionId = entry.sessionId ?? "";
5493
5573
  }
5494
- const compact = compactUserMessage(text);
5495
- currentUserMessage = compact.userMessage;
5496
- currentUserSignals = compact.userMessageSignals;
5497
- currentCalls = [];
5498
- currentTimestamp = entry.timestamp ?? "";
5499
- currentSessionId = entry.sessionId ?? "";
5500
- }
5501
- } else if (entry.type === "assistant") {
5502
- const msgId = getMessageId(entry);
5503
- const call = parseApiCall(entry);
5504
- if (!call) continue;
5505
- const previous = msgId ? seenCalls.get(msgId) : void 0;
5506
- if (previous) {
5507
- for (const key2 of Object.keys(call.usage)) {
5508
- previous.usage[key2] = Math.max(previous.usage[key2] ?? 0, call.usage[key2] ?? 0);
5574
+ } else if (entry.type === "assistant") {
5575
+ const msgId = getMessageId(entry);
5576
+ const call = parseApiCall(entry);
5577
+ if (!call) continue;
5578
+ const previous = msgId ? seenCalls.get(msgId) : void 0;
5579
+ if (previous) {
5580
+ for (const key2 of Object.keys(call.usage)) {
5581
+ previous.usage[key2] = Math.max(previous.usage[key2] ?? 0, call.usage[key2] ?? 0);
5582
+ }
5583
+ previous.tools = [.../* @__PURE__ */ new Set([...previous.tools, ...call.tools])];
5584
+ previous.mcpTools = extractMcpTools(previous.tools);
5585
+ previous.bashCommands = [.../* @__PURE__ */ new Set([...previous.bashCommands, ...call.bashCommands])];
5586
+ previous.hasAgentSpawn ||= call.hasAgentSpawn;
5587
+ previous.hasPlanMode ||= call.hasPlanMode;
5588
+ if (call.speed === "fast") previous.speed = "fast";
5589
+ previous.costUSD = calculateCost(
5590
+ previous.model,
5591
+ previous.usage.inputTokens,
5592
+ previous.usage.outputTokens,
5593
+ previous.usage.cacheCreationInputTokens,
5594
+ previous.usage.cacheReadInputTokens,
5595
+ previous.usage.webSearchRequests,
5596
+ previous.speed,
5597
+ previous.usage.inputTokens + previous.usage.cacheCreationInputTokens + previous.usage.cacheReadInputTokens,
5598
+ previous.usage.cacheCreation1hInputTokens
5599
+ );
5600
+ continue;
5509
5601
  }
5510
- previous.tools = [.../* @__PURE__ */ new Set([...previous.tools, ...call.tools])];
5511
- previous.mcpTools = extractMcpTools(previous.tools);
5512
- previous.bashCommands = [.../* @__PURE__ */ new Set([...previous.bashCommands, ...call.bashCommands])];
5513
- previous.hasAgentSpawn ||= call.hasAgentSpawn;
5514
- previous.hasPlanMode ||= call.hasPlanMode;
5515
- if (call.speed === "fast") previous.speed = "fast";
5516
- previous.costUSD = calculateCost(
5517
- previous.model,
5518
- previous.usage.inputTokens,
5519
- previous.usage.outputTokens,
5520
- previous.usage.cacheCreationInputTokens,
5521
- previous.usage.cacheReadInputTokens,
5522
- previous.usage.webSearchRequests,
5523
- previous.speed,
5524
- previous.usage.inputTokens + previous.usage.cacheCreationInputTokens + previous.usage.cacheReadInputTokens,
5525
- previous.usage.cacheCreation1hInputTokens
5526
- );
5527
- continue;
5602
+ if (msgId) seenCalls.set(msgId, call);
5603
+ else call.deduplicationKey = `${filePath}:${lineNumber}`;
5604
+ currentCalls.push(call);
5528
5605
  }
5529
- if (msgId) seenCalls.set(msgId, call);
5530
- else call.deduplicationKey = `${filePath}:${lineNumber}`;
5531
- chargeRecord();
5532
- currentCalls.push(call);
5533
5606
  }
5607
+ } catch (error) {
5608
+ if (error instanceof ResourceBudgetError) {
5609
+ try {
5610
+ await progress.save({ turns, currentCalls, currentUserMessage, currentUserSignals, currentTimestamp, currentSessionId, lineNumber }, offset);
5611
+ } catch {
5612
+ _parseWarnings.push("A historical transcript checkpoint could not be saved; its unfinished usage remains unknown.");
5613
+ }
5614
+ }
5615
+ throw error;
5534
5616
  }
5535
5617
  if (currentCalls.length > 0) {
5536
5618
  turns.push({
@@ -5635,7 +5717,7 @@ async function parseSessionFile(filePath, project, seenMsgIds, dateRange) {
5635
5717
  } catch {
5636
5718
  }
5637
5719
  }
5638
- const rawTurns = await cachedFileParse(filePath, "claude-turns", () => parseTurns(filePath));
5720
+ const rawTurns = await cachedFileParse(filePath, "claude-turns", (progress) => parseTurns(filePath, progress));
5639
5721
  const sessionId = basename9(filePath, ".jsonl");
5640
5722
  const turns = [];
5641
5723
  for (const turn of rawTurns) {
@@ -6976,6 +7058,7 @@ function isValidCache(parsed) {
6976
7058
  if (c.version !== DAILY_CACHE_VERSION && c.version !== 8) return false;
6977
7059
  if (typeof c.scopeKey !== "string" || c.scopeKey.length === 0) return false;
6978
7060
  if (!Array.isArray(c.days)) return false;
7061
+ if (c.historyScanProgress !== void 0 && (!Number.isSafeInteger(c.historyScanProgress) || c.historyScanProgress < 0)) return false;
6979
7062
  if (c.missingProviders && (!Array.isArray(c.missingProviders) || !c.missingProviders.every((p) => typeof p === "string"))) return false;
6980
7063
  if (c.historyBuild) {
6981
7064
  const stage = c.historyBuild;
@@ -7054,6 +7137,7 @@ function addNewDays(cache, incoming, newestDate, options = {}) {
7054
7137
  lastComputedDate: nextLast,
7055
7138
  days: merged,
7056
7139
  revalidatedAt: cache.revalidatedAt,
7140
+ historyScanProgress: cache.historyScanProgress,
7057
7141
  backfill: cache.backfill,
7058
7142
  partialDates: cache.partialDates
7059
7143
  };
@@ -10980,7 +11064,8 @@ function buildPeriodSnapshots(cache, todayDays, today, windows, provider, names,
10980
11064
  cache.historyBuild?.chunked,
10981
11065
  cache.missingProviders,
10982
11066
  cache.partialDates,
10983
- cache.backfill
11067
+ cache.backfill,
11068
+ cache.historyScanProgress
10984
11069
  ])).digest("hex"),
10985
11070
  message: window.key === "all" ? `Saved history through ${committed.at(-1).date}${live2.length ? ", plus today" : ""}. Some history is still missing; this is not the complete All total.` : `${hasData ? "Showing known usage" : "Recovering local usage"} for ${window.label}. ${covered} of ${required} historical days loaded${live2.some(recorded) ? ", plus today" : ""}. Missing days are unknown; this total is incomplete.`
10986
11071
  } : void 0;
@@ -11132,6 +11217,7 @@ async function completeHistoryBuild(cache, start, end, providers, scan, save2, o
11132
11217
  }
11133
11218
 
11134
11219
  // src/cli.ts
11220
+ init_parsed_file_cache();
11135
11221
  import { createInterface } from "readline";
11136
11222
  import { createRequire as createRequire3 } from "module";
11137
11223
 
@@ -11511,20 +11597,31 @@ program.command("status").description("Compact status output (today + week + mon
11511
11597
  const isAllProviders = pf === "all";
11512
11598
  const dailyCacheScope = buildDailyCacheScopeKey(opts.project, opts.exclude);
11513
11599
  const backfillWarnings = [];
11514
- const runChunkedBackfill = async (cache2, start, end, order) => {
11600
+ let savedParserProgress = parsedFileProgress();
11601
+ const saveHistoryCache = async (c) => {
11602
+ const progress = parsedFileProgress();
11603
+ const advanced = progress > savedParserProgress;
11604
+ c.historyScanProgress = (c.historyScanProgress ?? 0) + progress - savedParserProgress;
11605
+ savedParserProgress = progress;
11606
+ if (c.backfill) {
11607
+ c.backfill.attempts = advanced ? 0 : Math.min(c.backfill.attempts, MAX_BLOCKED_CHUNK_ATTEMPTS);
11608
+ }
11609
+ await saveDailyCache(c);
11610
+ };
11611
+ const runChunkedBackfill = async (cache2, start, end, order, repair = false) => {
11515
11612
  let c = cache2;
11516
11613
  const chunks = planBackfillChunks(start, end);
11517
11614
  if (order === "newest-first") chunks.reverse();
11518
11615
  for (const chunk of chunks) {
11519
11616
  const chunkDate = toDateString(chunk.start);
11520
11617
  const coveredThrough = toDateString(chunk.end);
11521
- if (shouldSkipBlockedChunk(c.backfill, chunkDate)) {
11618
+ if (!repair && shouldSkipBlockedChunk(c.backfill, chunkDate)) {
11522
11619
  const blanks = fillMissingDays(chunk.start, chunk.end, []);
11523
11620
  c = addNewDays(c, blanks, yesterdayStr, { coveredThrough });
11524
11621
  c.partialDates = [.../* @__PURE__ */ new Set([...c.partialDates ?? [], ...blanks.map((d) => d.date)])];
11525
11622
  c.backfill = null;
11526
11623
  c.revalidatedAt = Date.now();
11527
- await saveDailyCache(c);
11624
+ await saveHistoryCache(c);
11528
11625
  const warn2 = `history for ${chunkDate} exceeded the scan budget ${MAX_BLOCKED_CHUNK_ATTEMPTS} times; recorded as incomplete so newer days can load`;
11529
11626
  backfillWarnings.push(warn2);
11530
11627
  process.stderr.write(`[exe-watcher] WARNING: ${warn2}
@@ -11536,14 +11633,15 @@ program.command("status").description("Compact status output (today + week + mon
11536
11633
  const chunkProjects = filterProjectsByName(await parseAllSessions(chunkRange, "all", true), opts.project, opts.exclude);
11537
11634
  const chunkDays = fillMissingDays(chunk.start, chunk.end, aggregateProjectsIntoDays(chunkProjects));
11538
11635
  c = addNewDays(c, chunkDays, yesterdayStr, { coveredThrough });
11636
+ c.partialDates = (c.partialDates ?? []).filter((date2) => date2 < chunkDate || date2 > coveredThrough);
11539
11637
  c.backfill = null;
11540
11638
  c.revalidatedAt = Date.now();
11541
- await saveDailyCache(c);
11639
+ await saveHistoryCache(c);
11542
11640
  } catch (err) {
11543
11641
  if (!(err instanceof ResourceBudgetError)) throw err;
11544
11642
  c.backfill = nextBackfillCursor(c.backfill, chunkDate);
11545
11643
  c.revalidatedAt = Date.now();
11546
- await saveDailyCache(c);
11644
+ await saveHistoryCache(c);
11547
11645
  const warn2 = `history backfill paused at ${chunkDate} (attempt ${c.backfill.attempts}): ${err.message}`;
11548
11646
  backfillWarnings.push(warn2);
11549
11647
  process.stderr.write(`[exe-watcher] WARNING: ${warn2}
@@ -11564,7 +11662,7 @@ program.command("status").description("Compact status output (today + week + mon
11564
11662
  opts.project,
11565
11663
  opts.exclude
11566
11664
  )),
11567
- saveDailyCache,
11665
+ saveHistoryCache,
11568
11666
  { mergeExisting }
11569
11667
  );
11570
11668
  const pausedHistory = (c) => {
@@ -11574,6 +11672,7 @@ program.command("status").description("Compact status output (today + week + mon
11574
11672
  };
11575
11673
  const cache = await withScanBudget(() => withScanSlice(() => withDailyCacheLock(async () => {
11576
11674
  let c = await loadDailyCache(dailyCacheScope);
11675
+ const repairDates = [...new Set(c.partialDates ?? [])].sort();
11577
11676
  if (opts.period === "today" && opts.optimize === false) return c;
11578
11677
  if (c.missingProviders?.length && c.days.length && !c.needsRepricing) {
11579
11678
  const from = parseHistoryStartDate(c.days[0].date);
@@ -11619,6 +11718,17 @@ program.command("status").description("Compact status output (today + week + mon
11619
11718
  c = await runChunkedBackfill(c, clampedNeeded, backEnd, "newest-first");
11620
11719
  }
11621
11720
  }
11721
+ if (!c.backfill || repairDates.includes(c.backfill.blockedDate)) {
11722
+ const selectedStart = toDateString(periodInfo.range.start);
11723
+ for (const date2 of repairDates) {
11724
+ if (date2 < selectedStart || date2 > yesterdayStr || !c.partialDates?.includes(date2)) continue;
11725
+ const start = parseHistoryStartDate(date2);
11726
+ const end = new Date(start);
11727
+ end.setHours(23, 59, 59, 999);
11728
+ c = await runChunkedBackfill(c, start, end, "oldest-first", true);
11729
+ if (c.backfill) break;
11730
+ }
11731
+ }
11622
11732
  return c;
11623
11733
  }), { maxMs: 1e4, maxBytes: 8 * 1024 ** 3, maxRecords: 2e6 }), 2e6, void 0, 8 * 1024 ** 3);
11624
11734
  if (opts.optimize !== false || opts.period === "all" && !hasRecordedHistory(cache, toDateString(todayStart))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exe-watcher",
3
- "version": "0.2.67",
3
+ "version": "0.2.69",
4
4
  "description": "The owl watching your AI coding spend — cost, tokens, agents, and projects in your terminal and menu bar",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",