Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.

Commit 8f5ecff

Browse files
committed
refactor and fix output cache invalidation conditions
1 parent bedf8d4 commit 8f5ecff

7 files changed

Lines changed: 513 additions & 367 deletions

File tree

src/export.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,18 +136,19 @@ pub async fn run_export(format: ExportFormat, output: Option<String>, provider:
136136
Some(provider)
137137
};
138138

139+
let empty_prestats = std::collections::HashMap::new();
139140
let periods = vec![
140141
PeriodExport {
141142
label: "Today".to_string(),
142-
projects: parse_all_sessions(Some(&get_date_range("today")), pf)?,
143+
projects: parse_all_sessions(Some(&get_date_range("today")), pf, &empty_prestats)?,
143144
},
144145
PeriodExport {
145146
label: "7 Days".to_string(),
146-
projects: parse_all_sessions(Some(&get_date_range("week")), pf)?,
147+
projects: parse_all_sessions(Some(&get_date_range("week")), pf, &empty_prestats)?,
147148
},
148149
PeriodExport {
149150
label: "30 Days".to_string(),
150-
projects: parse_all_sessions(Some(&get_date_range("30days")), pf)?,
151+
projects: parse_all_sessions(Some(&get_date_range("30days")), pf, &empty_prestats)?,
151152
},
152153
];
153154

src/main.rs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,28 +77,8 @@ fn main() -> Result<()> {
7777
let stdin_tty = std::io::stdin().is_terminal();
7878
if !stdin_tty && static_only && !full_static {
7979
if let Some((period, provider)) = static_report_params(&command) {
80-
// Output memoization: when neither the report cache nor the
81-
// discovery cache has changed since the last run, we know the
82-
// rendered output would be byte-identical. Replay it straight
83-
// to stdout (~1 ms) and skip the whole parse pipeline.
84-
// --no-cache and --no-output-cache both bypass this.
85-
if !cli.no_cache && !cli.no_output_cache {
86-
let period_str = match period {
87-
cli::Period::Today => "today",
88-
cli::Period::Week => "week",
89-
cli::Period::ThirtyDays => "30days",
90-
cli::Period::Month => "month",
91-
};
92-
if output_cache::try_serve(period_str, &provider, "static", 0) {
93-
if prof {
94-
eprintln!(
95-
"[prof main] output-cache hit {:>8.2} ms",
96-
t_start.elapsed().as_secs_f64() * 1000.0,
97-
);
98-
}
99-
return Ok(());
100-
}
101-
}
80+
// `run_static_sync` memoizes output via `render_with_output_cache`,
81+
// which handles the fingerprint check + replay internally.
10282
return tui::run_static_sync(period, &provider);
10383
}
10484
}

src/output_cache.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
//! The fingerprint covers everything that can change the output:
2424
//! - mtime of `report-cache.bin` (any new parse miss bumps it)
2525
//! - mtime of `discovery.bin` (any new/deleted source bumps it)
26+
//! - session-files signature (hash of every known session file's
27+
//! mtime + size — catches appends to an
28+
//! existing session jsonl while the user
29+
//! keeps working in Claude / Cursor / etc.
30+
//! between runs, which don't bump the
31+
//! discovery-dir mtimes)
2632
//! - today's local date (period windows shift at the day boundary)
2733
//! - period + provider filter
2834
//!
@@ -88,7 +94,11 @@ fn today_local_packed() -> u64 {
8894
/// `extra` lets format-specific renderers fold things like terminal size
8995
/// into the fingerprint — for the rich dashboard a different `cols` /
9096
/// `rows` would produce different bytes, so the cache must invalidate.
91-
fn fingerprint(period: &str, provider: &str, format: &str, extra: u64) -> u64 {
97+
/// `session_sig` is the `u64` hash of every known session file's
98+
/// `(path, mtime, size)` triple (see `parser::stat_all_sources`) — this
99+
/// is what catches in-place appends to existing jsonl files when the
100+
/// user keeps working in Claude / Cursor / etc. between runs.
101+
fn fingerprint(period: &str, provider: &str, format: &str, extra: u64, session_sig: u64) -> u64 {
92102
let (rep_mtime, rep_size) = meta(&report_cache_path());
93103
let (disc_mtime, disc_size) = meta(&discovery_cache_path());
94104
let day = today_local_packed();
@@ -105,18 +115,24 @@ fn fingerprint(period: &str, provider: &str, format: &str, extra: u64) -> u64 {
105115
h.write_u64(rep_size);
106116
h.write_u64(disc_mtime);
107117
h.write_u64(disc_size);
118+
h.write_u64(session_sig);
108119
// Bake the cache file format version into the fingerprint so a binary
109120
// with a changed renderer can never serve stale output from before
110121
// its upgrade.
111122
h.write_u32(VERSION);
112123
h.finish()
113124
}
114125

115-
const VERSION: u32 = 2;
126+
// Bumped to 3: fingerprint now folds in a session-files signature so
127+
// in-place appends to existing jsonl files correctly invalidate.
128+
const VERSION: u32 = 3;
116129

117130
/// Try to serve the report straight from a previous run's cached output.
118131
/// Returns `true` if a hit was found and printed; the caller should exit.
119-
pub fn try_serve(period: &str, provider: &str, format: &str, extra: u64) -> bool {
132+
/// `session_sig` is the caller's freshly-computed session-files signature
133+
/// (see `parser::stat_all_sources`) — it's the only piece of the
134+
/// fingerprint that catches in-place appends to a session jsonl.
135+
pub fn try_serve(period: &str, provider: &str, format: &str, extra: u64, session_sig: u64) -> bool {
120136
let path = output_cache_path(period, provider, format);
121137
let bytes = match std::fs::read(&path) {
122138
Ok(b) => b,
@@ -136,7 +152,7 @@ pub fn try_serve(period: &str, provider: &str, format: &str, extra: u64) -> bool
136152
if 12 + len > bytes.len() {
137153
return false;
138154
}
139-
let want_fp = fingerprint(period, provider, format, extra);
155+
let want_fp = fingerprint(period, provider, format, extra, session_sig);
140156
if stored_fp != want_fp {
141157
return false;
142158
}
@@ -146,8 +162,8 @@ pub fn try_serve(period: &str, provider: &str, format: &str, extra: u64) -> bool
146162

147163
/// Persist the rendered output keyed on the current fingerprint. Called
148164
/// after a fresh compute so the next identical run can hit `try_serve`.
149-
pub fn store(period: &str, provider: &str, format: &str, extra: u64, output: &[u8]) {
150-
let fp = fingerprint(period, provider, format, extra);
165+
pub fn store(period: &str, provider: &str, format: &str, extra: u64, session_sig: u64, output: &[u8]) {
166+
let fp = fingerprint(period, provider, format, extra, session_sig);
151167
let path = output_cache_path(period, provider, format);
152168
if let Some(parent) = path.parent() {
153169
let _ = std::fs::create_dir_all(parent);

0 commit comments

Comments
 (0)