Files
orca/src/shared/filesystem-watch-batch-window.ts
T
Neil 5f126af9f8 fix(watcher): flush file changes on a steady cadence instead of waiting for quiet (#30270)
* perf(watcher): flush file changes on a steady cadence instead of waiting for quiet

Watcher batches used a 150 ms trailing debounce capped at 500 ms, so a worktree
with agents writing continuously always waited the full cap. The git status
nudge had no cap and could starve until the 60 s safety poll. Batches now flush
25 ms after the first change following a quiet period, then at most every 100 ms;
new events join an armed flush instead of pushing it back.

* fix(watcher): keep atomic saves in one batch and survive clock jumps

Raise the first-flush delay to 75 ms so the native watcher's split burst lands
in one batch, cap the flush delay at one interval so a backwards clock jump
can't stall delivery, and widen the editor delete debounce to cover a split
across one flush interval.

* test: wait past the widened editor delete debounce

* fix(watcher): keep markdown list refreshing under sustained writes and widen delete grace

Markdown document refresh used a resetting 125 ms debounce that never fired
once watch batches arrive every 100 ms. Delete tombstones now wait two batch
intervals so slow stat calls (WSL, AV scans) can't split a save into a flash.

* fix(watcher): bound watcher-driven reads and scans under sustained writes

One watcher-triggered editor read and one markdown scan per target in flight,
with a single follow-up; markdown scans honour the latest change time; the
headless Windows watcher no longer resets its refresh timer on every event.

* fix(watcher): make the faster remote cadence opt-in and harden watcher-driven reloads

files.watch takes an optional batchCadence; hosts keep legacy trailing batching
unless the client opts in. Coalesced editor follow-ups re-check the tab and
replace a hung read only once; Markdown mounts skip pre-change scans; the
Windows fallback refreshes the whole tree at most every 500 ms.

* fix(watcher): stop restarting the remaining watch-driven timers

Combined-diff section reloads and per-file editor reload dispatch no longer
push an armed timer back; a queued editor follow-up for a tab that left the
screen marks it stale instead of dropping the change.

* fix(watcher): let combined-diff and worktree notifications land under sustained writes

A combined-diff section discards an invalidated in-flight load at most once in a
row; worktree base-directory notifications no longer restart their timer.

* fix(watcher): never overwrite an edited diff section and unstick hung reloads

A combined-diff load never commits over a section with unsaved edits; a reload
queued behind a hung read starts after the stale limit without needing another
change event.

* fix(watcher): recover from hung markdown scans and keep evidence for edited tabs

A Markdown scan that never settles is abandoned after the join window so later
changes scan again; a queued editor follow-up for a tab edited meanwhile marks
it changed-on-disk and stale instead of dropping the change.

* chore(watcher): use a numeric markdown scan id

* fix(watcher): don't let later events join a stale echo-verification read

Payloads from the same burst still share the in-flight read; a payload from a
later task gets exactly one fresh read issued after the current one settles.
2026-10-18 23:44:18 -07:00

23 lines
1.1 KiB
TypeScript

// Why: the main-process watcher batcher and the runtime files.watch batcher
// share one flush window so local and remote latency can't drift.
// Why: a trailing debounce never fires while agents write continuously, so a
// busy worktree waited for the max-wait cap on every batch. Instead the first
// change after a quiet period flushes after a short coalescing tick, and a
// sustained stream flushes on a fixed cadence.
// Why 75: the native watcher sends a burst's first event alone and the rest
// ~50ms later; a shorter window splits atomic saves (tmp create + rename).
export const WATCH_BATCH_LEADING_MS = 75
export const WATCH_BATCH_INTERVAL_MS = 100
/** Delay before flushing a batch armed at `now`; `lastFlushAt` is 0 before the first flush. */
export function watchBatchFlushDelayMs(
now: number,
lastFlushAt: number,
leadingMs: number = WATCH_BATCH_LEADING_MS,
intervalMs: number = WATCH_BATCH_INTERVAL_MS
): number {
// Why cap at intervalMs: a backwards wall-clock jump must not stall delivery.
return Math.min(intervalMs, Math.max(leadingMs, lastFlushAt + intervalMs - now))
}