Skip to content

Commit 6d4bcf7

Browse files
committed
v0.1.44 - harden JSONL cache edge cases
1 parent d57b3ae commit 6d4bcf7

8 files changed

Lines changed: 209 additions & 30 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tally",
3-
"version": "0.1.43",
3+
"version": "0.1.44",
44
"private": true,
55
"scripts": {
66
"tauri": "tauri",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tally"
3-
version = "0.1.43"
3+
version = "0.1.44"
44
edition = "2021"
55
rust-version = "1.77"
66

src-tauri/src/claude/jsonl.rs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ struct FileSignature {
5151
local_ordinal: u32,
5252
}
5353

54+
impl FileSignature {
55+
fn new(local_now: chrono::DateTime<Local>) -> Self {
56+
Self {
57+
files: 0,
58+
bytes: 0,
59+
newest_mtime_ns: 0,
60+
local_year: local_now.year(),
61+
local_ordinal: local_now.ordinal(),
62+
}
63+
}
64+
65+
fn observe_file(&mut self, len: u64, modified: std::time::SystemTime) {
66+
if let Ok(delta) = modified.duration_since(std::time::UNIX_EPOCH) {
67+
self.newest_mtime_ns = self.newest_mtime_ns.max(delta.as_nanos());
68+
}
69+
self.files += 1;
70+
self.bytes = self.bytes.saturating_add(len);
71+
}
72+
}
73+
5474
fn stats_cache() -> &'static Mutex<Option<(FileSignature, ClaudeStats)>> {
5575
static CACHE: OnceLock<Mutex<Option<(FileSignature, ClaudeStats)>>> = OnceLock::new();
5676
CACHE.get_or_init(|| Mutex::new(None))
@@ -117,13 +137,7 @@ pub(crate) fn collect_token_stats() -> Result<ClaudeStats> {
117137
.unwrap_or(now);
118138

119139
let mut jsonl_files: Vec<(std::path::PathBuf, std::fs::Metadata)> = Vec::new();
120-
let mut signature = FileSignature {
121-
files: 0,
122-
bytes: 0,
123-
newest_mtime_ns: 0,
124-
local_year: now_local.year(),
125-
local_ordinal: now_local.ordinal(),
126-
};
140+
let mut signature = FileSignature::new(now_local);
127141

128142
for entry in walk_roots
129143
.iter()
@@ -143,12 +157,10 @@ pub(crate) fn collect_token_stats() -> Result<ClaudeStats> {
143157
if mt < cutoff_30d {
144158
continue;
145159
}
146-
if let Ok(delta) = mtime.duration_since(std::time::UNIX_EPOCH) {
147-
signature.newest_mtime_ns = signature.newest_mtime_ns.max(delta.as_nanos());
148-
}
160+
signature.observe_file(meta.len(), mtime);
161+
} else {
162+
signature.observe_file(meta.len(), std::time::UNIX_EPOCH);
149163
}
150-
signature.files += 1;
151-
signature.bytes = signature.bytes.saturating_add(meta.len());
152164
jsonl_files.push((path, meta));
153165
}
154166

@@ -273,3 +285,53 @@ fn add(w: &mut TokenWindow, u: &ClaudeUsage) {
273285
w.cache_read += u.cache_read_input_tokens;
274286
w.cache_write += u.cache_creation_input_tokens;
275287
}
288+
289+
#[cfg(test)]
290+
mod tests {
291+
use super::*;
292+
use chrono::TimeZone;
293+
294+
#[test]
295+
fn file_signature_changes_when_jsonl_tree_changes() {
296+
let day = Local.with_ymd_and_hms(2026, 5, 27, 8, 0, 0).unwrap();
297+
let mut a = FileSignature::new(day);
298+
a.observe_file(
299+
100,
300+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(10),
301+
);
302+
303+
let mut same = FileSignature::new(day);
304+
same.observe_file(
305+
100,
306+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(10),
307+
);
308+
assert_eq!(a, same);
309+
310+
let mut size_changed = FileSignature::new(day);
311+
size_changed.observe_file(
312+
101,
313+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(10),
314+
);
315+
assert_ne!(a, size_changed);
316+
317+
let mut mtime_changed = FileSignature::new(day);
318+
mtime_changed.observe_file(
319+
100,
320+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(11),
321+
);
322+
assert_ne!(a, mtime_changed);
323+
}
324+
325+
#[test]
326+
fn file_signature_changes_on_local_day_rollover() {
327+
let before_midnight = Local.with_ymd_and_hms(2026, 5, 27, 23, 59, 0).unwrap();
328+
let after_midnight = Local.with_ymd_and_hms(2026, 5, 28, 0, 1, 0).unwrap();
329+
let mut a = FileSignature::new(before_midnight);
330+
let mut b = FileSignature::new(after_midnight);
331+
let mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(10);
332+
a.observe_file(100, mtime);
333+
b.observe_file(100, mtime);
334+
335+
assert_ne!(a, b);
336+
}
337+
}

src-tauri/src/codex.rs

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,26 @@ struct FileSignature {
551551
local_ordinal: u32,
552552
}
553553

554+
impl FileSignature {
555+
fn new(local_now: chrono::DateTime<Local>) -> Self {
556+
Self {
557+
files: 0,
558+
bytes: 0,
559+
newest_mtime_ns: 0,
560+
local_year: local_now.year(),
561+
local_ordinal: local_now.ordinal(),
562+
}
563+
}
564+
565+
fn observe_file(&mut self, len: u64, modified: std::time::SystemTime) {
566+
if let Ok(delta) = modified.duration_since(std::time::UNIX_EPOCH) {
567+
self.newest_mtime_ns = self.newest_mtime_ns.max(delta.as_nanos());
568+
}
569+
self.files += 1;
570+
self.bytes = self.bytes.saturating_add(len);
571+
}
572+
}
573+
554574
#[derive(Debug, Default, Clone)]
555575
struct LocalTokenStats {
556576
today: CodexPeriodStats,
@@ -630,13 +650,7 @@ pub fn collect() -> Result<CodexStats> {
630650
}
631651

632652
let mut jsonl_files: Vec<(PathBuf, std::fs::Metadata)> = Vec::new();
633-
let mut signature = FileSignature {
634-
files: 0,
635-
bytes: 0,
636-
newest_mtime_ns: 0,
637-
local_year: now_local.year(),
638-
local_ordinal: now_local.ordinal(),
639-
};
653+
let mut signature = FileSignature::new(now_local);
640654
for entry in roots
641655
.iter()
642656
.flat_map(|r| WalkDir::new(r).into_iter())
@@ -655,12 +669,10 @@ pub fn collect() -> Result<CodexStats> {
655669
if mt < cutoff_30d {
656670
continue;
657671
}
658-
if let Ok(delta) = mtime.duration_since(std::time::UNIX_EPOCH) {
659-
signature.newest_mtime_ns = signature.newest_mtime_ns.max(delta.as_nanos());
660-
}
672+
signature.observe_file(meta.len(), mtime);
673+
} else {
674+
signature.observe_file(meta.len(), std::time::UNIX_EPOCH);
661675
}
662-
signature.files += 1;
663-
signature.bytes = signature.bytes.saturating_add(meta.len());
664676
jsonl_files.push((path, meta));
665677
}
666678

@@ -795,3 +807,56 @@ fn apply_local_token_stats(stats: &mut CodexStats, local: &LocalTokenStats) {
795807
stats.d30 = local.d30.clone();
796808
stats.mtd = local.mtd.clone();
797809
}
810+
811+
#[cfg(test)]
812+
mod local_cache_tests {
813+
use super::*;
814+
use chrono::TimeZone;
815+
816+
#[test]
817+
fn file_signature_changes_on_file_or_day_changes() {
818+
let day = Local.with_ymd_and_hms(2026, 5, 27, 8, 0, 0).unwrap();
819+
let next_day = Local.with_ymd_and_hms(2026, 5, 28, 8, 0, 0).unwrap();
820+
let mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(10);
821+
822+
let mut base = FileSignature::new(day);
823+
base.observe_file(42, mtime);
824+
825+
let mut same = FileSignature::new(day);
826+
same.observe_file(42, mtime);
827+
assert_eq!(base, same);
828+
829+
let mut size_changed = FileSignature::new(day);
830+
size_changed.observe_file(43, mtime);
831+
assert_ne!(base, size_changed);
832+
833+
let mut day_changed = FileSignature::new(next_day);
834+
day_changed.observe_file(42, mtime);
835+
assert_ne!(base, day_changed);
836+
}
837+
838+
#[test]
839+
fn cached_local_token_stats_do_not_overwrite_live_limit_fields() {
840+
let mut stats = CodexStats {
841+
plan_label: "PRO 5x".to_string(),
842+
plan_label_raw: "prolite".to_string(),
843+
last_event_at: Some(Utc::now()),
844+
..Default::default()
845+
};
846+
stats.rate_limits.primary_used_percent = 12.0;
847+
let local = LocalTokenStats {
848+
today: CodexPeriodStats {
849+
requests: 7,
850+
..Default::default()
851+
},
852+
..Default::default()
853+
};
854+
855+
apply_local_token_stats(&mut stats, &local);
856+
857+
assert_eq!(stats.today.requests, 7);
858+
assert_eq!(stats.rate_limits.primary_used_percent, 12.0);
859+
assert_eq!(stats.plan_label_raw, "prolite");
860+
assert!(stats.last_event_at.is_some());
861+
}
862+
}

src-tauri/src/history.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,56 @@ mod tests {
330330
assert_eq!(usage.requests, 5);
331331
assert_eq!(usage.api_equiv, 12.50);
332332
}
333+
334+
#[test]
335+
fn daily_upsert_reports_no_change_for_lower_or_equal_usage() {
336+
let existing = DailyUsage {
337+
tokens: DailyTokens {
338+
input: 100,
339+
output: 50,
340+
cache_read: 20,
341+
cache_write: 10,
342+
cached_input: 30,
343+
reasoning: 40,
344+
},
345+
requests: 5,
346+
api_equiv: 12.50,
347+
};
348+
let incoming = DailyUsage {
349+
tokens: DailyTokens {
350+
input: 90,
351+
output: 50,
352+
cache_read: 20,
353+
cache_write: 10,
354+
cached_input: 30,
355+
reasoning: 40,
356+
},
357+
requests: 5,
358+
api_equiv: 12.50,
359+
};
360+
let mut slot = Some(existing.clone());
361+
362+
assert!(!upsert_vendor_day(&mut slot, &incoming));
363+
assert_eq!(slot.unwrap().tokens.input, existing.tokens.input);
364+
}
365+
366+
#[test]
367+
fn daily_upsert_reports_change_for_growth_or_new_day() {
368+
let incoming = DailyUsage {
369+
requests: 1,
370+
api_equiv: 1.0,
371+
..Default::default()
372+
};
373+
let mut empty = None;
374+
assert!(upsert_vendor_day(&mut empty, &incoming));
375+
376+
let mut existing = Some(incoming.clone());
377+
let larger = DailyUsage {
378+
requests: 2,
379+
api_equiv: 3.0,
380+
..Default::default()
381+
};
382+
assert!(upsert_vendor_day(&mut existing, &larger));
383+
assert_eq!(existing.unwrap().requests, 2);
384+
}
333385
}

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "TALLY - Ai Usage Monitor",
4-
"version": "0.1.43",
4+
"version": "0.1.44",
55
"identifier": "com.cjmedia.tally",
66
"build": {
77
"frontendDist": "../src"

0 commit comments

Comments
 (0)