8000 fix(diff): bound the snapshot walk and slow the new-file discovery cadence by capad-xyz · Pull Request #138 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content

fix(diff): bound the snapshot walk and slow the new-file discovery cadence - #138

Merged
amirlehmam merged 1 commit into
amirlehmam:masterfrom
capad-xyz:fix/snapshot-walk-budget
Aug 2, 2026
Merged

amirlehmam merged 1 commit into
amirlehmam:masterfrom
capad-xyz:fix/snapshot-walk-budget

Conversation

@capad-xyz
Copy link
Copy Markdown
Contributor

Follow-ups (1) and (2) from #133, which were deliberately left out of #135 as design calls rather than bug fixes. (3) — whether to offer the fallback at all for home-like trees — is still yours; happy to implement whatever you land on.

(1) Bound the walk, not just the read

SNAPSHOT_IGNORE gains the platform directories: AppData, Library, Application Data, $RECYCLE.BIN, System Volume Information, OneDrive, Recovery.

More importantly, MAX_SNAPSHOT_DIRS (500) is now threaded through walkDir as a mutable budget shared across the recursion, so traversal is bounded independently of the file cap. This is the half you identified as the important one: the 2,000-file cap bounds how much we read, but ~3,578 directories were still enumerated before it bit, and the resulting 2,000 files were an accident of traversal order.

(2) Slower discovery cadence

The re-walk that finds new files now runs every 5th poll (REWALK_EVERY_N_POLLS). The stat-based check for existing files still runs every poll.

Behaviour change worth calling out explicitly: a newly added file now appears within ~10s instead of ~2s. Modifications and deletions are unchanged — they come from the snapshot map and are still detected on the very next poll. If you want a different number, it's a one-line constant.

There's a trap in this change that isn't obvious: because newly discovered files are not in the snapshot map, running discovery only every Nth poll makes them flicker — present on poll 5, absent on 6–9, present on 10. So discovered additions are now remembered per-cwd in addedPaths and re-reported on every poll (and dropped when the file goes away). resetSnapshot clears that alongside the snapshot. Two tests pin this: one that an added file keeps appearing across 12 consecutive polls, one that it disappears once deleted.

Also: the prefix check you noted

resolveWithin used resolved.startsWith(path.resolve(cwd)), which accepts C:\foobar as inside C:\foo. The untracked-file branch of getFileDiff had the identical test three lines away. Both now go through one isWithin helper built on path.relative.

Neither was reachable — as you said, the paths come from a walk of cwd itself — so this is hygiene, not a security fix. Flagging it as slightly outside what you asked for; it seemed odd to fix one and leave its twin adjacent to it.

Tests

tests/unit/diff-provider.test.ts grows from 5 to 9 cases:

  • added files surface once the periodic re-walk runs
  • an added file keeps being reported between re-walks (no flicker)
  • a discovered file drops out of the list once deleted
  • AppData/** is never reported, across several polls

The pre-existing "modified, added, deleted" case was split in two, because added-file detection is deliberately no longer immediate and the old case asserted that it was. Modified/deleted still assert next-poll detection.

Full suite: 61 files, 667 tests passing. npm run typecheck and npm run lint clean.

…dence

Follow-ups (1) and (2) from amirlehmam#133, left out of amirlehmam#135 as design calls.

The 2000-file cap bounds how much the snapshot fallback READS but not how
much it WALKS: on a home directory ~3,500 directories are enumerated before
the cap bites, so which files you end up watching is an accident of
traversal order.

- SNAPSHOT_IGNORE gains platform directories (AppData, Library, Application
  Data, $RECYCLE.BIN, System Volume Information, OneDrive, Recovery)
- MAX_SNAPSHOT_DIRS budget threaded through the walk, so traversal is
  bounded independently of the file cap
- re-walk for new files now runs every 5th poll instead of every poll; the
  stat-based content check still runs every poll. Added files therefore
  surface within ~10s rather than ~2s; modifications and deletions are
  unaffected
- discovered additions are remembered per-cwd so they do not flicker out of
  the list on the polls between re-walks
- resolveWithin and the untracked-file branch of getFileDiff both used a
  startsWith prefix test that would accept C:\foobar as inside C:\foo; both
  now share an isWithin helper built on path.relative
@amirlehmam
Copy link
Copy Markdown
Owner

Merged in 970f7b8. Verified locally first: 61 files / 667 tests, typecheck clean — exactly what you reported.

Both halves are right, and (1) is the one I'd have got wrong on my own. #135 bounded how much we read; it left the walk itself unbounded, so the 2,000 files we ended up with were an accident of traversal order — a MAX_SNAPSHOT_FILES that bites after 3,578 directories isn't a budget, it's a coincidence. Threading MAX_SNAPSHOT_DIRS through the recursion as shared mutable state is the correct shape for that, and it's the kind of thing that reads as a small diff and isn't.

On (2): the flicker trap is the part of this PR I'm most glad you wrote down. Making discovery periodic while modification detection stayed per-poll creates two classes of file with different visibility rules, and the failure — present on poll 5, gone on 6–9, back on 10 — would have looked like a rendering bug for a long time before anyone suspected the poll cadence. addedPaths plus the two tests that pin it across 12 consecutive polls is the right answer; splitting the old "modified, added, deleted" case in two rather than loosening it was also right, since the whole point is that those three no longer share a timing contract.

~10s to notice a new file is fine. Modifications and deletions are what people actually watch a diff pane for, and those are unchanged at one poll.

The resolveWithin prefix check: taking it. You flagged it as slightly outside scope and it is, but leaving one of an adjacent pair fixed is worse than fixing both, and path.relative is the version that doesn't need a comment explaining why C:\foobar isn't inside C:\foo.

On (3) — I'm not going to offer the fallback for home-like trees. A diff pane pointed at %USERPROFILE% has no useful answer to give; the honest thing is to say so rather than to compute a bounded-but-arbitrary subset that looks like a real answer. I'll open that separately if it turns out people actually do it by accident.

Thank you — that's #133, #135 and now #138, and the standard hasn't dropped once.

bchap1n added a commit to bchap1n/wmux that referenced this pull request Aug 10, 2026
- agent-state declared blocked/working/idle panes (amirlehmam#128), consent-gated
  agent integration (amirlehmam#132), i18n audit + community locales (amirlehmam#136 amirlehmam#147),
  key remaps (amirlehmam#145), diff pane fixes (amirlehmam#133 amirlehmam#138 amirlehmam#141), orphaned PTY reaping
  (amirlehmam#139), V2 CLI caller scoping (amirlehmam#143), browser state in update backup (amirlehmam#146)
- OMP fork kept: WMUX_AGENT_CMD=omp workers, OMP context/extension install
  (still unconditional at startup; Claude/OpenCode features now consent-gated),
  agent-label notifications via i18n keys, portable build scripts (em-dash -> --)
- opencode-context: shared injectWmuxBlock + upstream stripWmuxBlock/remove paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0