cmux-next: project pickers check each folder once per answer, not once per Git file

Merged
TE
16d ago
+150-9

The project pickers (New Tab page, Start Agent, the composer's folder menu) ask the host for up to 40 folders' repositories (project.list repos) and each folder's icon (project.icon). Every Git file and icon candidate the host reads first goes through ChatRootValidator: refusal walks each folder of the path from / with readlink, and reachesProtectedFolder lstats each folder plus every protected folder. Paths in one answer share almost all their folders (/Users, the home folder, the projects folder), so one answer made tens of thousands of these syscalls, re-reading the same few folders.

ChatRootValidator.batch() returns a copy that reads each folder's link and identity once. The repo walk (AgentPaneProjectRepoReader.repos(for:)) and each icon answer (AgentPaneProjectIcons.answer(for:)) use one batch, dropped with the answer, so nothing is kept between answers and the checks see the disk as it is when the picker asks. Answers don't change.

Before

Live nightly 1.0.0-nightly.3966508748201 (CMUXCommit 331e2e9), pid 50854, cosmic-gray at load average ~170. sample 50854 5, symbolized with that build's dSYM artifact (UUID 545871AE matches the binary). In one of three samples, three utility-QoS threads each spent the entire 5 s (1882 of 1882 samples) in:

closure #1 in static AgentPaneProjectRepoReader.replies(for:reader:)  AgentPaneProjectRepo.swift:64
  AgentPaneProjectRepoReader.repos(for:) → repo(for:) → repo(at:) → text(_:)
    ChatRootValidator.refusal(_:)               ChatRootValidator.swift:35  → readlink   2788 samples
    ChatRootValidator.reachesProtectedFolder(_:) ChatRootValidator.swift:62  → lstat      2091 samples
    the actual file reads (FileHandle open/read)                                            ~660 samples

So the privacy checks were about 86% of that walk's time, the Git files it reads about 12%: three cores busy for at least 5 s for one picker opening.

After

Not measured yet: this needs the app built, and this Mac builds nothing. Expected from the walk's shape: a folder 4 to 8 levels deep checked ~3 Git files per level for up to 6 levels; with the batch each distinct folder is read once instead of once per path through it, and the 31 protected-folder lstats happen once per answer instead of once per path, so the syscalls per answer should drop by an order of magnitude. I'll re-run the same sample while opening a New Tab page on the nightly that carries this.

Tests

ChatRootValidatorBatchTests (CmuxNextSettingsTests): a batch over 10 paths in one tree asks readlink about each folder once (33 lookups, none repeated); a batch answers refusal and reachesProtectedFolder exactly as the validator does for the home folder, a project, a link into Documents, Documents itself, Documentſ, the data-volume spelling, / and a relative path. Existing AgentPaneProjectRepoTests and AgentPaneProjectIconTests keep covering the readers (their fakes survive batched(), which copies the reader).

Observed / Not run

Observed: the sample above; scripts/cmux-next/prepush-gate.py reports no new violations. Not run: the Swift build and tests (CI runs them); no after-number yet.

No capture: no visual change; the pickers show the same repositories and icons, only how often the host re-reads the same folders changes.

Changelog

  • Fixed: opening a project picker no longer keeps several CPU cores busy re-checking the same folders.

Agent session: cosmic-gray

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes the project pickers (New Tab page, Start Agent, the composer's folder menu) re-reading the same folders thousands of times per answer. Each Git file and icon candidate was checked with ChatRootValidator, which walks every folder from / for each path; on a loaded Mac, three CPU cores could stay busy for seconds on one picker opening.

ChatRootValidator.batch() returns a copy that reads each folder's link and identity once, and the protected-folder lookups once per answer. The repo walk and each icon answer use one batch dropped when the answer is done, so answers stay the same while the disk is unchanged. Within one picker answer, the icon walk and repo walk share the same batch when they guard the same home folder. Batch lookups are atomic (two threads never read the same path) and stop remembering past 4096 paths, so a huge icon set stays bounded.

  • Expected syscall reduction per answer: about an order of magnitude.