docs: D3 framing revision 3 --- the scheduler becomes a state machine

Review round 2: two P1 design gaps and one P2 overclaim, all in the
cadence revision 2 introduced.

A join now wakes the group: next_scan_at pulls to now, an in-flight
walk queues exactly one immediate follow-up, and a baseline is only a
snapshot whose WALK STARTED after the join --- an in-flight walk may
have passed a directory before a pre-join file appeared there, so its
snapshot as a baseline would turn that file into a false CREATED.
Without the wake, a backed-off group folds post-registration files
into the baseline and never reports them; today registration scans
immediately and the coalesced design must not regress that.

The group is now a defined state machine: single-flight per group
(overlap unrepresentable, not avoided), deadlines advanced from
completion (a walk outliving its interval degrades to back-to-back
scans, never overlap), stale completions rejected by generation
(#234 P2 at group scope), and retirement --- last member gone or
server death --- that cooperatively cancels the walk. Cancellation
therefore enters walk_tree contract and tests; polling the cancel
token between directory reads is the established job shape. The
after-tick subscription installs once and guards, because
pmacs.hook.remove does not exist (the P3 gap).

Q#D3-1 restated honestly: groups key on (server, base), so several
can be due on one frame and per-group single-flight still permits N
jobs. The bar offered is absence at idle plus one attributable job
per concurrently due group, with a global scan queue as the
alternative if one-at-a-time must be guaranteed.

Six round-2 witnesses join the plan: join-wakes, no-overlap,
retirement, queued-baseline, plus the round-1 epoch and idle pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-08-11 12:48:41 +02:00
parent 0aac3b8992
commit 9c644b0ae7
No known key found for this signature in database
2 changed files with 197 additions and 85 deletions

View File

@ -250,9 +250,20 @@ the same day (`b867f64`), refreshed and re-gated on the merged base.
**D3 — the polling cost — is the remainder, and the user has ruled it **D3 — the polling cost — is the remainder, and the user has ruled it
is next (2026-08-11).** **Branch `lsp-file-watch-d3`** (base is next (2026-08-11).** **Branch `lsp-file-watch-d3`** (base
`githubsucks/main` @ `add0ba1`; the remote ref is authoritative), with `githubsucks/main` @ `add0ba1`; the remote ref is authoritative), with
**framing `docs/lsp-file-watch-d3-framing.md`, revision 2, DRAFT — **framing `docs/lsp-file-watch-d3-framing.md`, revision 3, DRAFT —
awaiting review**, committed at the branch's first commit so it is awaiting review**, committed at the branch's first commit so it is
portable during review. **Review round 1 (2026-08-11) found five portable during review. **Review round 2 (2026-08-11) found the
scheduler underspecified**: a joining watcher must force an immediate
baseline scan (a backed-off group would otherwise fold
post-registration files into the baseline — and a baseline is now
only a snapshot whose WALK STARTED after the join); the group gained
a defined state machine (single-flight per group, deadlines advanced
from completion, stale completions rejected by generation, retirement
that cooperatively cancels the walk — cancellation joining
`walk_tree`'s contract); and Q#D3-1's `⋯1` was an overclaim — the
accurate bar is absence at idle plus one attributable job per
concurrently due (server, base) group. Six round-2 witnesses joined
the plan. **Review round 1 (2026-08-11) found five
findings and revision 1 did not survive it** — the promised idle findings and revision 1 did not survive it** — the promised idle
state was impossible (`workers.sleep` is a pool-thread-holding state was impossible (`workers.sleep` is a pool-thread-holding
running job the indicator counts; revision 2 replaces the sleep loop running job the indicator counts; revision 2 replaces the sleep loop

View File

@ -1,6 +1,6 @@
# LSP file watcher D3 — the polling cost — framing # LSP file watcher D3 — the polling cost — framing
**Status: revision 2 — DRAFT, awaiting review. No implementation may **Status: revision 3 — DRAFT, awaiting review. No implementation may
begin from this document.** begin from this document.**
Continues issue #233, which stays open until this lane closes it. D1 Continues issue #233, which stays open until this lane closes it. D1
@ -25,7 +25,7 @@ exists to catch before code does.
(`src/async_runtime.rs:1570`). A 4 s backoff sleep would render as a (`src/async_runtime.rs:1570`). A 4 s backoff sleep would render as a
*constant* `⋯1 sleep 4000ms`, and filtering sleeps from the *constant* `⋯1 sleep 4000ms`, and filtering sleeps from the
indicator would touch the instrument this lane declares out of indicator would touch the instrument this lane declares out of
scope. **Revision 2 removes the sleep from the design entirely** scope. **Revision 2 removed the sleep from the design entirely**
(see "The cadence" below) — the fix is the codebase's own idiom, (see "The cadence" below) — the fix is the codebase's own idiom,
not a new mechanism. not a new mechanism.
- **P1 — the scan root must be the server's, not a freshly detected - **P1 — the scan root must be the server's, not a freshly detected
@ -38,7 +38,7 @@ exists to catch before code does.
root). The server's own `root_uri` and `cwd` are already exposed root). The server's own `root_uri` and `cwd` are already exposed
(`pmacs.lsp.list`, `src/lua_bindings/mod.rs:10985-11002`; (`pmacs.lsp.list`, `src/lua_bindings/mod.rs:10985-11002`;
`root_uri` is the spec field verbatim, nil when the server never `root_uri` is the spec field verbatim, nil when the server never
asked for a root). **Revision 2 roots the scan at what the asked for a root). **Revision 2 rooted the scan at what the
registering server actually serves**: `root_uri``cwd` registering server actually serves**: `root_uri``cwd`
attached-file directory, in that order. attached-file directory, in that order.
- **P1 — coalescing needs registration-epoch semantics.** Revision 1 - **P1 — coalescing needs registration-epoch semantics.** Revision 1
@ -47,9 +47,8 @@ exists to catch before code does.
snapshots would receive a false CREATED for a file that predates snapshots would receive a false CREATED for a file that predates
its registration; membership changing during a walk recreates its registration; membership changing during a walk recreates
either #234's stale-watcher batch or the same pre-registration either #234's stale-watcher batch or the same pre-registration
event. **Revision 2 defines shared snapshots with per-watcher event. **Revision 2 defined shared snapshots with per-watcher
baselines** (below), plus two witnesses the existing six tests do baselines**, refined by round 2 below.
not cover.
- **P1 — "VCS-only exclusion is safest" was wrong.** A hard skip - **P1 — "VCS-only exclusion is safest" was wrong.** A hard skip
silently ignores a server that legitimately registers `.git/HEAD` silently ignores a server that legitimately registers `.git/HEAD`
or `**/.git/**`, and glob semantics mean even `**/*.rs` *can* match or `**/.git/**`, and glob semantics mean even `**/*.rs` *can* match
@ -57,24 +56,61 @@ exists to catch before code does.
the registered contract, not a safe default. Revision 1 also the registered contract, not a safe default. Revision 1 also
overweighted the win: exclusion was a **job-count** lever when every overweighted the win: exclusion was a **job-count** lever when every
directory was a separate job, and the walk primitive removes that directory was a separate job, and the walk primitive removes that
economics. **Revision 2 defaults to no unconditional exclusion** economics. **Revision 2 defaults to no unconditional exclusion.**
and reframes Q#D3-2 around the full option set.
- **P2 — the arithmetic used the issue's machine, not this one.** - **P2 — the arithmetic used the issue's machine, not this one.**
With D = 220 on this checkout it is 220 `read_dir` jobs **plus one With D = 220 on this checkout it is 220 `read_dir` jobs **plus one
sleep** per watcher per tick — 221, or **1,326 across sleep** per watcher per tick — 221, or **1,326 across
rust-analyzer's six watchers** — and revision 1's proposed steady rust-analyzer's six watchers** — and revision 1's proposed steady
state was itself two jobs (sleep + walk), not one. Corrected state was itself two jobs (sleep + walk), not one. Corrected
throughout; the revised design's steady state is **zero jobs at throughout.
idle** and one `walk_tree` job while a scan runs.
## Review round 2 — the scheduler was underspecified
Round 2 (2026-08-11) closed the round-1 findings and found the
cadence's own semantics missing: revision 2 said *when* scans become
due but not what happens when due-ness, in-flight walks, joins, and
retirement collide.
- **P1 — a joining watcher must force an immediate baseline scan.**
Revision 2's baseline was "the first snapshot completed after
join" — but a backed-off group's next snapshot can be 4 s away, so
a file created after registration and before that delayed snapshot
would fold into the baseline and never be reported. Today,
registration begins its initial scan immediately (`lsp.lua:2074`);
the coalesced design must preserve that. **A join pulls the group's
`next_scan_at` to now; if a walk is already in flight, exactly one
immediate follow-up scan is queued.** The baseline is sharpened to
match: a snapshot serves as a watcher's baseline only if its **walk
started after the join** — an in-flight walk may have passed a
directory before a pre-join file appeared there, and using its
snapshot as a baseline would turn that file into a false CREATED on
the next diff.
- **P1 — single-flight and retirement were undefined.** The document
itself establishes that walks can outlive their interval, so
"every due group starts a scan" permits overlapping walks —
restoring multiple jobs, completing snapshots out of order, and
making the epoch state ambiguous. **The group scheduler below is
now a defined state machine**: one in-flight scan per group,
deadlines advanced from completion, stale completions rejected,
and retirement (last member gone, or server death) that
cooperatively cancels the walk — which puts cancellation into
`walk_tree`'s contract and tests.
- **P2 — `⋯1` was an overclaim.** Groups are keyed by
(server, base), so several can be due on the same frame, and
per-group single-flight still permits `⋯N`. **The bar is restated
accurately**: absence at idle; while scans run, one attributable
job per concurrently due group. Global serialization is offered as
the alternative under Q#D3-1 if `⋯1` must be guaranteed.
## Verified against the tree at `add0ba1` ## Verified against the tree at `add0ba1`
Every claim below was read or measured this session (revision 2 Every claim below was read or measured this session (revisions 2 and
re-verified the round-1 corrections against the code). 3 re-verified their corrections against the code).
- Each registered watcher is its own coroutine looping - Each registered watcher is its own coroutine looping
`sleep(FILE_WATCH_INTERVAL_MS)``scan_tree` (`lsp.lua:1924`, `sleep(FILE_WATCH_INTERVAL_MS)``scan_tree` (`lsp.lua:1924`,
`:2074-2083`); the interval is 250 ms. `:2074-2083`); the interval is 250 ms. Registration's initial scan
runs immediately (`:2074-2075`).
- `scan_tree` awaits `pmacs.fs.read_dir` once **per directory** - `scan_tree` awaits `pmacs.fs.read_dir` once **per directory**
(`:2038-2041`), one async job each. `walk` recurses (`:2038-2041`), one async job each. `walk` recurses
unconditionally; `matches` gates only whether an entry is unconditionally; `matches` gates only whether an entry is
@ -111,6 +147,14 @@ re-verified the round-1 corrections against the code).
loops (`lua_bindings/mod.rs:13833`). `autosave.lua`'s Q#AS2 sweep loops (`lua_bindings/mod.rs:13833`). `autosave.lua`'s Q#AS2 sweep
is the model: one clock read and one compare per frame, no job, no is the model: one clock read and one compare per frame, no job, no
pool thread. pool thread.
- **`pmacs.hook.remove` does not exist** (the P3 prerequisite gap,
`docs/agent-handoff.md` §1a) — an after-tick subscription is
permanent, so the scheduler installs **once** and early-returns
when it owns no groups, exactly as autosave's does when disabled.
- **Cooperative cancellation is the established job shape**: every
job body in `async_runtime.rs` polls `cancel.is_cancelled()` at
its work boundaries (`run_sleep` per slice, the others per unit);
`walk_tree` polling between directory reads inherits the pattern.
- **No `notify`/inotify dependency in the tree** and **no - **No `notify`/inotify dependency in the tree** and **no
ignore-list infrastructure to reuse** — both re-verified, both ignore-list infrastructure to reuse** — both re-verified, both
carried from the D1/D2 framing. carried from the D1/D2 framing.
@ -121,26 +165,59 @@ Not "quiet the modeline." The indicator is the instrument that found
this, and the churn it shows is real; quieting it is explicitly out this, and the churn it shows is real; quieting it is explicitly out
of bounds. The lane's job is to make the background work **small, of bounds. The lane's job is to make the background work **small,
attributable, and honest**: at idle there should *be* no running attributable, and honest**: at idle there should *be* no running
background work to report, and while a scan runs it should be one job background work to report, and while scans run each should be one job
named for its root. named for its root.
## The cadence — after-tick deadlines, not sleeps (review P1) ## The cadence — after-tick deadlines, not sleeps (round 1)
The per-watcher sleep loop is replaced by the Q#AS2 idiom: one The per-watcher sleep loop is replaced by the Q#AS2 idiom: one
`process.after-tick` subscription owns every scan group's schedule. `process.after-tick` subscription owns every scan group's schedule.
Per frame it reads `pmacs.editor.monotonic_ms` once and compares each Per frame it reads `pmacs.editor.monotonic_ms` once and compares each
group's `next_scan_at`; a due group gets its scan started (a group's `next_scan_at`. Waiting allocates **no job and no pool
coroutine that runs the walk and diff). Waiting therefore allocates thread** and renders **no indicator segment**`activity_summary`
**no job and no pool thread** and renders **no indicator segment** returns `None` at zero by contract. While a scan runs, the indicator
`activity_summary` returns `None` at zero by contract. While a scan honestly shows its job.
runs, the indicator honestly shows its one job.
This also retires a defect revision 1 did not name: today's sleeps The subscription installs once at module load and early-returns when
hold pool threads, so N watchers subtract N threads from a pool of no groups exist — it cannot be removed, because `pmacs.hook.remove`
`available_parallelism - 1`. The after-tick cadence gives them all does not exist, and a guard is the house answer (autosave's
back. `enabled` check).
## The scan root (review P1) ### The group state machine (round 2)
Per group — keyed (server, base) — the scheduler holds
`next_scan_at`, the backoff interval, an **in-flight flag**, a
**scan generation counter**, and a `rescan_queued` bit.
- **Single-flight.** The after-tick check skips a group whose walk is
in flight; a group cannot become due against itself. Concurrent
walks, out-of-order snapshots, and ambiguous epochs are therefore
unrepresentable, not merely avoided.
- **Deadlines advance from completion.** On scan completion,
`next_scan_at = completion time + current interval`. A walk that
outlives its interval degrades to back-to-back scans with a full
interval between them — never to overlap.
- **Stale completions are rejected.** Each scan carries its group's
generation at start; a completion whose group is retired, or whose
generation is not the group's current one, is dropped before any
state write or emit — #234's P2 recheck, applied at group scope.
- **Joins wake the group.** A watcher joining sets
`next_scan_at = now`. If a walk is in flight, `rescan_queued` is
set instead, and completion of the current walk starts **exactly
one** immediate follow-up scan. The joiner's baseline is the first
snapshot whose **walk started after its join** (see below), so the
baseline is at most one walk-duration away — never a backoff cap
away. The join-triggered scan does **not** reset the backoff curve;
only observed changes do.
- **Retirement.** When the last member leaves (unregistration, or
supersession with no successor) or the server dies, the group
retires: the in-flight walk's job is **cancelled cooperatively**,
its completion is rejected by the generation rule, and the group's
schedule entry and snapshots are dropped. A re-registration that
replaces members keeps the group alive — the superseded members
are cancelled per #234's D2 and the new members join as above.
## The scan root (round 1)
For a string-form (bare `*.txt` / absolute) registration the base For a string-form (bare `*.txt` / absolute) registration the base
becomes, in order: the server's **`root_uri`** (spec verbatim — nil becomes, in order: the server's **`root_uri`** (spec verbatim — nil
@ -152,28 +229,28 @@ never reproduce (Q#LX2). This replaces the `pairs()`-order accident
with a deterministic, server-owned answer. `RelativePattern`s keep with a deterministic, server-owned answer. `RelativePattern`s keep
their own `baseUri`, unchanged. their own `baseUri`, unchanged.
## Coalescing, with registration epochs (review P1) ## Coalescing, with registration epochs (rounds 1 and 2)
One scan group per (server, base). The group's scanner records One scan group per (server, base). The group's scanner records
**all** files (the matcher moves from scan time to diff time); each **all** files (the matcher moves from scan time to diff time); each
completed scan increments the group's **snapshot epoch**. completed scan increments the group's **snapshot epoch**.
Delivery semantics, stated precisely because revision 1 did not: Delivery semantics:
- Each watcher records the epoch current when it **joined** the - Each watcher's **baseline is the first snapshot whose walk started
group. Its **baseline is the first snapshot completed after it after it joined** — an in-flight walk may have passed a directory
joined**; it receives diffs only between snapshots it has a before a pre-join file appeared there, so its snapshot cannot serve
baseline for. A file created after the group's previous snapshot as a baseline (round 2). A watcher receives diffs only between
but before a new watcher registered therefore produces **no event snapshots at or after its baseline. A file created after the
for that watcher** — it is folded into the watcher's baseline, group's previous snapshot but before a watcher joined therefore
produces **no event for that watcher** — folded into its baseline,
exactly as the initial scan folds pre-existing files today. exactly as the initial scan folds pre-existing files today.
- **Membership is captured at scan start**; a watcher joining - **Membership for delivery is captured at scan start**; a watcher
mid-walk waits for the next snapshot. joining mid-walk waits for its queued baseline scan.
- **Cancellation is rechecked per watcher at emit time**#234's P2 - **Cancellation is rechecked per watcher at emit time**#234's P2
rule, now applied per member: a watcher superseded or unregistered rule per member: a watcher superseded or unregistered during the
during the walk emits nothing, and its replacement (a fresh join) walk emits nothing, and its replacement has no baseline yet, so it
has no baseline yet, so it emits nothing either. Both halves of the emits nothing either.
round-1 hazard close on the same two rules.
- Changes passing a watcher's matcher and kind mask are deduped by - Changes passing a watcher's matcher and kind mask are deduped by
`(uri, type)` into the server's single `(uri, type)` into the server's single
`workspace/didChangeWatchedFiles` notification, as today. `workspace/didChangeWatchedFiles` notification, as today.
@ -184,11 +261,17 @@ Delivery semantics, stated precisely because revision 1 did not:
instead of one per directory: 220 `read_dir` jobs per scan on this instead of one per directory: 220 `read_dir` jobs per scan on this
repo become 1. The indicator shows one purpose (`walk_tree <root>`). repo become 1. The indicator shows one purpose (`walk_tree <root>`).
An additive fs binding plus its async-runtime job; **no wire change** An additive fs binding plus its async-runtime job; **no wire change**
(fs bindings are not the frontend protocol) and no new crate. Symlink (fs bindings are not the frontend protocol) and no new crate. Two
non-traversal (`scan_tree`'s loop-safety) moves into the primitive's contract clauses, each with its own Rust tests:
contract, witnessed by its own Rust tests.
## Exclusions (review P1) — none by default - **Symlinks are recorded, not traversed**`scan_tree`'s
loop-safety, preserved.
- **Cancellation is cooperative and prompt**: the job polls its
cancel token between directory reads (the established
`async_runtime.rs` job shape), so group retirement mid-walk stops
the walk instead of orphaning it.
## Exclusions (round 1) — none by default
Glob semantics make any unconditional skip a contract deviation: Glob semantics make any unconditional skip a contract deviation:
`**/*.rs` compiles with a separator-spanning prefix, so it *can* `**/*.rs` compiles with a separator-spanning prefix, so it *can*
@ -198,28 +281,30 @@ match under `.git/`, and a server may register `.git/HEAD` outright
economics support it: exclusion was worth 80 % of the *job count* economics support it: exclusion was worth 80 % of the *job count*
when every directory was a job; inside one `walk_tree` job it is only when every directory was a job; inside one `walk_tree` job it is only
readdir syscalls, and the whole 220-directory walk is a few readdir syscalls, and the whole 220-directory walk is a few
milliseconds of one pool thread every backoff interval. milliseconds of one pool thread per scan.
The option space, for Q#D3-2: (a) no unconditional exclusion — the The option space, for Q#D3-2: (a) no unconditional exclusion — the
proposed default; (b) **opt-in** exclusion through configuration, for proposed default; (b) **opt-in** exclusion through configuration, for
users with pathological trees, framed explicitly as a watcher-contract users with pathological trees, framed explicitly as a
trade; (c) matcher-aware pruning — skip a subtree only when *no* watcher-contract trade; (c) matcher-aware pruning — skip a subtree
active watcher's pattern can match under it — which is sound but only when *no* active watcher's pattern can match under it — sound
almost never fires against real registrations, because but almost never fires against real registrations, because
`**/`-leading globs can match anywhere; (d) a hard built-in VCS skip, `**/`-leading globs can match anywhere; (d) a hard built-in VCS
which revision 1 called "safest" and is not: it is (b) without the skip, which revision 1 called "safest" and is not: it is (b) without
opt-in. the opt-in.
## Idle backoff ## Idle backoff
The interval doubles while consecutive scans observe no change, The interval doubles while consecutive scans observe no change,
capped at 4 s; any change batch resets it to 250 ms. Under the capped at 4 s; any change batch resets it to 250 ms. Under the
after-tick cadence a longer interval costs *nothing* while waiting — after-tick cadence a longer interval costs *nothing* while waiting —
backoff now bounds **scan frequency**, not sleep-job length. backoff bounds **scan frequency**, not sleep-job length. Worst-case
Worst-case latency for an external change at idle equals the cap; latency for an external change at idle equals the cap **except at
LSP imposes no latency bound, and edits made through pmacs never registration, where the join rule forces an immediate baseline**
depended on the watcher (the server sees `didChange`). The watcher (round 2). LSP imposes no latency bound, and edits made through
exists for git checkouts, generated files, and other editors. pmacs never depended on the watcher (the server sees `didChange`).
The watcher exists for git checkouts, generated files, and other
editors.
## Deliberately staged separately — kernel notification ## Deliberately staged separately — kernel notification
@ -233,26 +318,30 @@ deserves its own review.
## Proposed shape — Stage 1 ## Proposed shape — Stage 1
After-tick cadence + walk primitive + coalescing-with-epochs + After-tick cadence with the group state machine + walk primitive +
backoff; no exclusions by default; server-owned scan root. coalescing-with-epochs + backoff; no exclusions by default;
server-owned scan root.
At rest on this repo with rust-analyzer attached: **from 1,326 jobs At rest on this repo with rust-analyzer attached: **from 1,326 jobs
per scan-bound tick (six of them pool-thread-holding sleeps) to zero per scan-bound tick (six of them pool-thread-holding sleeps) to zero
jobs at idle**, with one `walk_tree` job for the few milliseconds a jobs at idle**, with one `walk_tree` job per group for the few
scan actually runs, at most every 250 ms under activity and every 4 s milliseconds each scan actually runs — at most every 250 ms under
at rest. activity and every 4 s at rest, immediately once at registration.
## Open rulings — each blocks implementation ## Open rulings — each blocks implementation
- **Q#D3-1 — the acceptance bar.** With Stage 1 the indicator is - **Q#D3-1 — the acceptance bar, stated accurately (round 2).** At
**absent at idle** (no running job exists — `activity_summary`'s idle the indicator is **absent** (no running job exists —
`None`-at-zero contract) and shows `⋯1 walk_tree <root>` for the `activity_summary`'s `None`-at-zero contract). While scans run it
duration of each scan. Is that the bar — an honest blip per scan, shows **one attributable job per concurrently due group**`⋯N`
absence otherwise — with true event-driven silence deferred to the when N (server, base) groups are due on the same frame, each named
kernel-notification framing? for its root; a typical single-project session has one group.
Alternative if `⋯1` must be guaranteed: a global scan queue
serializing walks across groups, at the cost of coupling one
server's scan latency to another's tree size. Which bar?
- **Q#D3-2 — exclusions.** Proposed: none by default, with opt-in - **Q#D3-2 — exclusions.** Proposed: none by default, with opt-in
exclusion as a documented contract trade (option b) if a user asks. exclusion as a documented contract trade (option b) if a user
Confirm, or rule for one of (b)/(c)/(d) above. asks. Confirm, or rule for one of (b)/(c)/(d) above.
- **Q#D3-3 — the scan root.** Proposed: server `root_uri` → server - **Q#D3-3 — the scan root.** Proposed: server `root_uri` → server
`cwd` → attached-file directory. This widens the watched tree for `cwd` → attached-file directory. This widens the watched tree for
servers with a real root (today it is one attached file's servers with a real root (today it is one attached file's
@ -268,31 +357,43 @@ at rest.
- **Idle witness:** with a server attached, watchers registered, and - **Idle witness:** with a server attached, watchers registered, and
no file activity, `activity_summary` settles to `None` (the absent no file activity, `activity_summary` settles to `None` (the absent
segment) between scans — the strongest form of the job-count claim, segment) between scans — the strongest form of the job-count
and unwritable under the sleep design. claim, and unwritable under the sleep design.
- **Scan-cost witness:** one scan allocates O(1) jobs, not - **Scan-cost witness:** one scan allocates O(1) jobs, not
O(directories), on a tree with enough directories to discriminate. O(directories), on a tree with enough directories to discriminate.
- **Join-wakes witness (round 2):** with a group backed off at the
cap, register a new watcher — a scan starts immediately
(timestamps through the group seam), and a file created after the
join is reported to the joiner from its baseline onward.
- **No-overlap witness (round 2):** with a walk deliberately held
in flight past its interval (through the group seam or by
withholding the completion pump), the group allocates **no second
walk job**; deadlines resume from completion.
- **Retirement witness (round 2):** the last member unregisters
mid-walk — the walk's job is cancelled, its completion is
rejected (no emit, no state write), and the group's schedule entry
is gone; a server death takes the same path.
- **Queued-baseline witness (round 2):** a watcher joining mid-walk
gets exactly one immediate follow-up scan, and its baseline is
that scan, not the walk that was in flight at join.
- **Epoch witness (registration between snapshots):** create a file - **Epoch witness (registration between snapshots):** create a file
after the group's snapshot, then register a second watcher, then after the group's snapshot, then register a second watcher, then
let a scan complete — the old watcher receives CREATED, the new one let a scan complete — the old watcher receives CREATED, the new
receives **nothing** for that file, and does receive events for one receives **nothing** for that file, and does receive events
files created after its baseline. for files created after its baseline.
- **Epoch witness (replacement during a scan):** through the group's
scan seam (the `_after_scan_for_tests` device, lifted to the
group), re-register mid-scan — the superseded watcher emits
nothing (P2's rule, per member) and the replacement emits nothing
until its own baseline exists.
- **Backoff witness:** quiet scans lengthen the gap between scans - **Backoff witness:** quiet scans lengthen the gap between scans
and one change resets it — observed through scan timestamps at the and one change resets it — observed through scan timestamps at the
seam, not through sleep purposes (there are none). seam, not through sleep purposes (there are none).
- **Root witness:** a server with a configured root watches that - **Root witness:** a server with a configured root watches that
root, not the attached file's directory; texlab's resolver shape is root, not the attached file's directory; texlab's resolver shape
the fixture model. is the fixture model.
- **Contract preservation:** all six existing `m4_24` watcher tests - **Contract preservation:** all six existing `m4_24` watcher tests
stay **byte-unchanged** and green. stay **byte-unchanged** and green.
- `walk_tree` Rust unit tests: symlinks recorded-not-traversed, - `walk_tree` Rust unit tests: symlinks recorded-not-traversed,
signature parity with the Lua walk it replaces. cooperative cancellation observed mid-walk, signature parity with
- Each new behaviour is mutation-tested against the defect it guards. the Lua walk it replaces.
- Each new behaviour is mutation-tested against the defect it
guards.
## Coherence impact (§20) ## Coherence impact (§20)