Repository navigation
Stop scanning the host in App::new, and once per file in --clean - #85
Merged
Merged
Conversation
ndr-ds
marked this pull request as ready for review
August 30, 2026 01:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
App::new()ended withapp.refresh()— a full synchronous scan of the real~/.claude(discovery::scan_sessionsplus apsshell-out viaprocess::fetch_and_enrich), then ledger rollups and a detached background scan thread. Constructing an App was an I/O operation, and there are ~26 construction sites.Nobody wanted it
Two workarounds for it were already in the tree:
src/main.rs:823—// Re-refresh to replace real sessions discovered during App::new(), thenapp.refresh(). In demo mode the constructor scanned the real host and the very next call threw the result away for generated sessions.app_with_empty_data()— "bypassing the host-side session discovery thatApp::new()does in its constructor", thenreplace_data(AppData::default()). Every test using it paid for a full scan and discarded it.So the scan was load-bearing for exactly one caller — the non-demo TUI — and pure waste for the rest.
What it cost
Measured in this sandbox, where
~/.claudeis a virtiofs bind mount:refresh_nonblockingtestscargo test --libThe "after" numbers were taken under load average 167, higher than the 98 during the "before" run, so this is not a quiet-box artifact.
It also made
refresh_nonblocking_kicks_worker_under_runtimefail outright — 238 s against its own 60 s deadline — then pass at 75/38/32 s in isolation. That deadline had already been widened once. CI never caught it because CI's~/.claudeis empty.The bug the constructor was hiding
run_clean's--finishedfilter built an entire App per JSONL file, inside a doubly-nested loop, purely to ask whether one path belonged to a live session. With 4577 transcripts on this box that is 4577 full host scans and 4577psforks:--clean --finishedwas effectively unusable. Both liveness questions now come from the one scanrun_cleanalready performs:The predicate is unchanged —
any(|s| s.jsonl_path == file_path)becomesset.contains(&file_path)over the same session list. The one semantic difference: liveness is now sampled once at the start rather than re-sampled per file. Since the sweep went from ~9.5 hours to 7 s, that snapshot is far fresher than what the per-file rescan actually delivered.The change
App::new()constructs and nothing else. (It still readsparked.json— that is one small file, and the doc comment says so rather than claiming the constructor is pure.)App::with_host_state()is exactly today'sApp::new()— scan, rollups, background scan, initial row selection.data_snapshot().sessionsmoves towith_host_state():main.rsTUI startup and five incommands.rs. No production behaviour changes.make_app's demo branch keeps the plainApp::new(): it already overwrote sessions and the ledger fields with..AppData::default(), andapply_filters— whichmake_appcalls — is what actually establishes the selected row, not the constructor.Appgainsrefresh_worker: fn(Vec<AgentSession>) -> RefreshIoOutput, defaulting todo_refresh_io. The two tests that exercise scheduling and channel plumbing — not discovery — inject a trivial in-memory worker, so they no longer touch the host. Their 60 s deadline becomes a 5 s hang guard, which is what it always should have been.RefreshIoOutputderivesDefaultso that injected worker is one line.app_with_empty_data()is deleted: with a pure constructor it was a synonym forApp::new()whosereplace_data(AppData::default())had become a no-op.Verification
cargo clippy --all-targets -- -D warningsclean; 767 lib tests pass.Review found three things, all fixed above: the
--cleanloop, a doc comment claiming the constructor touches nothing when it still readsparked.json, and the stale helper comment. The--cleanfix is verified end to end by running the real binary against 4577 files, not by reasoning.