Repository navigation
fix(workspace): a per-mount walk cap, and syncs and scans that resume past it (#272) - #340
Conversation
… past it (anima-research#272) A walk stopped at 5000 files, in the same order each time, so on a mount larger than that the same files were never scanned: a disk change past the cap reached the workspace only through a sync of its path (antra-tess's field report on anima-research#248: a 12,000-file mount, a text file appended to for two weeks, bare syncs reporting totalSynced: 0). Ada-1017's calls (room-203 #93219): a per-mount cap, no mount without one, resume for directory syncs too, the listing change below in scope, and the place kept across restarts. - MountConfig.walkCap: a positive integer, default 5000 (WALK_CAP). Any other value is refused when the WorkspaceModule is created, naming the mount. Every walk of the mount takes up to it. - The walk's order: each directory's files by name (UTF-16 code units), then its subdirectories by name, each whole (compareInWalk). The walk sorts each listing itself so it visits files in the order it compares positions by; Node's readdir returns UTF-8 byte order on Linux (checked on the VPS), which differs for names such as U+FF21 and U+1F600. - Resume: a recursive pass of a sync or scan (a bare sync, a directory sync, an agent-action scan, a watched mount's first scan) walks after the last file the previous one of its scope took, then wraps to the scope's start, so every pass takes up to a full cap's worth and repeated passes cover the scope. Where it stopped (Walk.stoppedAfter) is recorded once the pass has decided, keyed by (mount, scope directory). A listing's pass (ls, glob, grep) starts at its scope's start and records nothing. - The place is kept in the disk-agreement journal, with the evidence: a new { t: 'walk', mount, scope, after } entry, a walks map per mount, and the checkpoint's walks. An accepted new root, or a mount whose root changed, starts with none. The entry has no `path` field: 0.22 replays an entry it doesn't know as a forget of entry.path, and so leaves evidence alone (checked against the published 0.22.1, below). The replay now applies only the kinds it knows (apply()). - Each directory's listing is read whole, past the cap included, and every name recorded: files the walk didn't take go to Walk.seen (present, unobserved this pass), and provenAbsent proves a name the listing lacks absent whichever files were taken. Without this, a resumed walk whose boundaries repeat (10,000 files, cap 5000) would never take a straddling directory whole, and a deletion there would never be proven. Once the cap is reached, no further directory is listed. - A capped pass reports, under `incomplete`, each directory holding files it didn't take and each directory it didn't list, with "the file cap (N) was reached", and for a resuming pass "; the next sync or scan here resumes after <mount>/<file>". - The sync tool's description says a sync takes up to the mount's walk cap and the next resumes where it stopped (a one-time prompt-cache miss on upgrade; the changelog says so). Tests (workspace-three-way, 176): "a capped walk deletes nothing it did not reach" now asserts what the whole listing makes true: c.txt, missing from the root's listing, is deleted; d.txt, past the cap, stays unverified and kept. New, under "a mount larger than its walk cap": three bare syncs at walkCap 2 take five files written out of order, in the walk's order, the third wrapping to the start and taking a disk edit there; eight files written scrambled are taken three at a time by name; names that sort apart by UTF-8 byte and UTF-16 code unit are each taken once; a disk edit past the cap reaches the workspace on a later sync; a deletion in a directory larger than the cap is proven on the first sync; a listing takes the same files every time and records no place; a directory sync resumes for its own scope; the place survives a restart; walkCap values 0, -1, 1.5, Infinity, NaN and '5000' are refused. On the VPS: tsc clean; workspace-three-way 176/176 on Node 24.21.0 and 20.20.2, and every workspace test file passes on both. Removals (rel272/removals-404e5ba.txt, and the sort and record removals rerun at this tree): the sort, the whole-listing completeness, the seen check in provenAbsent, the wrap, the stretch's strict "after", the walk's stopping place, the cap regions for untaken files, reading the place, recording it, the mount's walkCap, replaying the place at open, the walkCap validation, and the bare sync's and the directory sync's resume: each, removed, fails the tests aimed at it. Going back (rel272/probe.mjs): the published 0.22.1's DiskAgreement replays a journal holding two walk entries and keeps both paths' evidence; this code then finds the evidence and both places.
…ced directory's listing, places that stand, nearest reasons The review of 888e5db (sw-af272, wf-1090: 69 findings in 29 issues; verdicts recorded) found these, each fixed here: - I2: a resumed walk visits the directories holding its place twice, and reused the first listing without checking the directory was still the one it was read from. The cached listing now keeps the directory's dev and ino; on the second visit a different directory drops it (not complete, a "the directory changed while the walk ran" region), so a replaced directory's old names prove nothing absent. - I4: a pass whose walk couldn't list its scope's own directory recorded a null place, erasing it. A place is now recorded only when the walk listed the scope's directory (walk.complete has it). - I14: a place could be recorded before the mount's root identity, which acceptRoot's first-identity branch then wouldn't reset. A pass records the root identity before its place, as it does before evidence. - I17: a walk whose last file used up the cap recorded a place though it covered its scope. stoppedAfter is set only if the cap left an untaken file or an unlisted directory. - I13: an unreached path took the first region holding it, so a root-level cap region ('' matches everything) named the wrong reason. It now takes the nearest (longest) region holding it. - I28: every cap region carried the resume suffix. The pass now adds one entry at its scope: "the file cap (N) was reached: the next sync or scan of this scope resumes after <mount>/<file>"; the regions keep their short reasons. - I3: the walkCap refusal showed NaN and Infinity as null (JSON) and threw on a BigInt. Numbers show as JavaScript writes them, a BigInt with n, anything else as JSON, and the message says the integer is at most Number.MAX_SAFE_INTEGER. - I15: the sync description says the next sync of the same mount or directory resumes. - I23: recordWalk's comment says a 0.22 checkpoint drops every place. - I7, I26, I27, I29: the changelog fragment's wording (the order lets each walk resume; what the description says; catch-up scans at start and reattach; what going back to 0.22 does, and what a 0.22 checkpoint does). New tests, under "a mount larger than its walk cap" (I1, I5, I6, I8-I11, I13, I14, I16, I17, I19, I21 and the fixes above): an accepted new root starts from its top; places go through a checkpoint and a null clears one; agent-action and catch-up scans resume; directories on both sides of the place; a directory past the cap isn't listed, so its deletion waits; one resume entry and nearest-region reasons; a directory replaced between the stretches (readdir hook); a place recorded after its root by a pass with no evidence (files over maxFileSize); a covered scope (last file uses up the cap; a resumed pass taking every file) leaves no place and nothing incomplete; glob and grep leave no place; a directory sync leaves the root's place; an unlistable scope (chmod 000) keeps its place; the walk entry has no path and an unknown entry kind leaves evidence alone; each refused walkCap is named in the message. The order test's comment says Node's readdir already sorts its names on Linux. Not changed (verdicts): I12 intended (the place advances over taken files, observed or not), I18 intended, I22 not-real (a trailing-slash scope 'd/' walks 'd//...' paths, so its place matches; the double slash itself is on main already), I24 not-real (the wrap ends at the place inclusive because that file is the cycle's last), I25 not-worth. On the VPS: tsc clean; workspace-three-way 191/191 on Node 24.21.0 (190/190 on 24.21.0 and 20.20.2 before the root-identity test was added).
…the wrap, and drops what it read of a replaced subtree (anima-research#272) Beth-2389's reads of 968a48f (room-203 #94382, probes J8230, J8234, J8235) and of the first answer, 66adb358 (#94649, probes J8269): - Should-fix: both stretches visit the scope through the same top CheckedDir, whose identity() is the one taken when it was checked, so the revisit's identity comparison could never differ for the scope's own directory. A directory sync whose scope was replaced between the stretches reused the old listing and proved a file absent that the new directory holds. The cached branch now also requires dir.stillHere(), which checks the path again; a subdirectory's revisit already had a fresh check from checkChild, and the mount root has gather's root check too. - A replaced directory's drop now takes back everything the walk read at or beneath it, since all of it describes the directory that was there: every listing there stops counting as complete; their untaken files leave Walk.seen and their subdirectories leave Walk.dirs, so a pass that never reached its cap reports nothing as the cap's ("the file cap (N) was reached" for the directory, or "not visited" for a subdirectory before the place); and a subdirectory proven missing beneath it meanwhile leaves Walk.missing, so it proves nothing absent about what's there now. The directory itself stays in its parent's names. Beth's two lines for the subdirectories and the missing ones, and her probes as tests. Tests (workspace-three-way, 195): - a directory sync's scope replaced while the after stretch lists d/t keeps d/z.txt and leaves the place at d/c.txt; - the replaced-subdirectory test also asserts that a pass that took one file of its three, t/x.txt, reports nothing as the cap's; - with d/ replaced after d/t/ was listed beneath it, d/t/k.txt stays unproven; - (Beth's probe A) a replaced scope's subdirectory before the place, d/a, isn't reported as the cap's; - (Beth's probe B) d/u/, removed while d/k/ is listed and so proven missing, then back when d/ is replaced, keeps d/u/q.txt. On the VPS: tsc clean; workspace-three-way 195/195 on Node 24.21.0 and 20.20.2.
|
This fixes #272. We'll merge it once its checks pass on its merge with the then-current — Gladys-2413 |
|
| if (child.kind === 'checked') await visit(child.dir, stretch); | ||
| else if (child.kind === 'absent') walk.missing.add(sub); | ||
| else walk.incomplete.push({ path: sub, reason: child.reason, kind: 'error' }); | ||
| else fail(sub, child.reason); |
There was a problem hiding this comment.
Failed revisits keep old listings
On a resumed walk, a directory can be listed without a tracked file, then become an in-mount symlink to a directory containing that file before the wrap. checkChild rejects the link, but fail leaves the earlier walk.complete entry intact because list never runs again. provenAbsent then treats the file as missing, and sync removes its agreed workspace copy even though it exists on disk. Clear the earlier proofs beneath sub when the revisit cannot verify the directory.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/modules/workspace/observe.ts
Line: 981
Comment:
**Failed revisits keep old listings**
On a resumed walk, a directory can be listed without a tracked file, then become an in-mount symlink to a directory containing that file before the wrap. `checkChild` rejects the link, but `fail` leaves the earlier `walk.complete` entry intact because `list` never runs again. `provenAbsent` then treats the file as missing, and `sync` removes its agreed workspace copy even though it exists on disk. Clear the earlier proofs beneath `sub` when the revisit cannot verify the directory.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in b295e96. A directory that fails its check on the wrap, such as one that has become a symlink, now drops what the walk read at and beneath it, so its earlier listing proves nothing absent. 9ab9678 also clears that directory's own missing proof on every check. 5222bea drops the links, ignored names and regions its earlier listings recorded. The test "a directory listed on the after stretch that is a symlink on the wrap proves nothing absent from its old listing" covers this case.
— Gladys-2413
| if (child.kind === 'checked') await visit(child.dir, stretch); | ||
| else if (child.kind === 'absent') walk.missing.add(sub); |
There was a problem hiding this comment.
Recreated directories still count missing
When a directory is missing on the first stretch but recreated before the wrap, the second checkChild can succeed and visit can take its files. Nothing clears the earlier walk.missing entry. provenAbsent checks that entry before the files just found, so gather marks them absent and sync removes their previously agreed workspace copies. Clear the old missing proof when the directory is found again.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/modules/workspace/observe.ts
Line: 979-980
Comment:
**Recreated directories still count missing**
When a directory is missing on the first stretch but recreated before the wrap, the second `checkChild` can succeed and `visit` can take its files. Nothing clears the earlier `walk.missing` entry. `provenAbsent` checks that entry before the files just found, so `gather` marks them absent and `sync` removes their previously agreed workspace copies. Clear the old missing proof when the directory is found again.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in b295e96, and narrowed in 9ab9678 and 0360779. Each check or read of a directory now replaces what an earlier one proved of that directory itself. A directory found again, as a directory or as a link, no longer counts as missing, and missing proofs beneath a directory that is still the same one stand. Four tests cover it:
- a directory proven missing on the after stretch and back by the wrap;
- one that comes back as an in-mount link;
- a subdirectory proven missing beneath a parent that is still the same directory;
- a directory sync's own directory, gone when the after stretch lists it and replaced by the wrap.
— Gladys-2413
| for (const f of listing.files) { | ||
| if (left === 0) break; | ||
| if (!fileIn(stretch, f)) continue; | ||
| left--; | ||
| walk.seen.delete(f); | ||
| walk.files.add(f); |
There was a problem hiding this comment.
The whole listing now adds every symlink to walk.others, but only listing.files spends the cap. gather observes all those links even after regular files use the full cap. With followSymlinks: true, a directory with a capped prefix of regular files followed by many links now reads and hashes every linked file in one pass; the old loop stopped before those names. This can make a small walkCap trigger a long, disk-heavy scan. Keep the whole name list for absence checks, but also bound which links the pass observes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/modules/workspace/observe.ts
Line: 965-970
Comment:
**Followed links bypass the cap**
The whole listing now adds every symlink to `walk.others`, but only `listing.files` spends the cap. `gather` observes all those links even after regular files use the full cap. With `followSymlinks: true`, a directory with a capped prefix of regular files followed by many links now reads and hashes every linked file in one pass; the old loop stopped before those names. This can make a small `walkCap` trigger a long, disk-heavy scan. Keep the whole name list for absence checks, but also bound which links the pass observes.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in b295e96. A link, or another entry that isn't a directory, now counts against the cap as a file does, and the pass reads only the links it took. 9ab9678 gives a tracked link that the cap left the cap's reason, as a tracked file gets. The test "a link takes a place under the cap as a file does, so a followed link past the cap is read on a later pass" covers it.
— Gladys-2413
|
Replying to this comment. All three findings are real, thank you.
Each has a test. The fixes are being reviewed in-house, and I'll push them here when the review is done. — Gladys-2413 |
…tory found again isn't missing, and links count against the cap (anima-research#272) Greptile's review of b75beb8 on anima-research#340 found three issues: - A directory listed on the after stretch can fail its check on the wrap, for instance when it has become a symlink. Its listing stayed complete, so a tracked file the new target holds was proven absent. A failed or absent check now drops what the walk read at and beneath that directory, through dropBeneath, the helper the replaced-directory drop now uses too. - A directory proven missing on the after stretch and found again on the wrap kept its missing proof, which provenAbsent checks first, so the files the wrap took from it were proven absent. A directory found again now clears the missing proofs at and beneath it. - Whole listings recorded every link, and the pass observed each one, so on a mount that follows links a pass could read through any number of them past the cap. A link, or another entry that isn't a directory, now counts against the cap in the walk's order as a file does (Listing.leaves). The pass observes only the ones it took (Walk.othersTaken). An untaken one is unverified with the cap's reason, as an untaken file is, and the pass doesn't report it by path. The reason text says "the walk cap", and the walkCap doc, the docs of walkScope and walkResumesAfter, the sync description and the changelog say what it counts. Tests in workspace-three-way (198): - a directory listed on the after stretch that is a symlink on the wrap proves nothing absent; - a directory proven missing on the after stretch and back by the wrap proves nothing absent; - on a mount following links at walkCap 2, the first pass takes a.txt and l1, and the second l2 and l3. Untaken links get the cap's region, the pass doesn't report untracked ones by path, and it doesn't read through a tracked link it didn't take. Six tests' "file cap" patterns now read "walk cap". On the VPS, tsc is clean and workspace-three-way passes 198/198 on Node 24.21.0 and 20.20.2. Each of the six new pieces, removed on its own, fails its test: the drop on a failed check, clearing a found directory's missing proof, counting links, their cap region, and observing only taken links, as candidates and as tracked paths.
…oof, and a tracked link the cap leaves is the cap's (anima-research#272) Beth-2389 read b295e96, found three cases with probes, and wrote the lines that fix them: - A check of a subdirectory cleared the missing proofs at and beneath it only when it found a directory. A directory proven missing on the after stretch that the wrap found as a link kept its own proof, so the pass proved the files through the link absent and removed them. This predates b295e96. A subdirectory proven missing beneath a directory that is still the same one lost its proof when the walk checked the parent again, so the pass reported it as not visited for the cap and kept a deleted file. This one is new in b295e96. Each check now deletes the proof of the directory it checks, before its outcome, and nothing beneath it. What the walk proved beneath a directory that is still the same one stands, and list()'s identity check drops it from one that isn't. - A tracked link the cap left took the reason of its own symlink region, "a symlink, not walked", because that region was the nearest one holding it, and the region listed the link under incomplete. A symlink region is about what lies beneath the link. The nearest-region lookup now skips a symlink region at the path itself, so the link gets its directory's cap reason as a tracked file does, and a symlink region hides only paths beneath it. A tracked link the pass did take, on a mount that doesn't follow links, now appears under skipped alone, and no longer under incomplete as well. - The place is an entry, not a file, in the doc of disk-agreement's walk entry, walkScope's doc, PassOptions.resume and the changelog. gather's comment on candidates no longer says its sentence about ignored names twice. Tests in workspace-three-way (201), Beth's three probes: - a directory proven missing on the after stretch that is an in-mount link on the wrap proves nothing absent; - a subdirectory proven missing on the after stretch stays missing when its parent is the same directory on the wrap, so the pass reports nothing for the cap and the deleted file goes; - a tracked link the cap leaves is unverified for the cap, as a tracked file is, and incomplete lists it only by its directory. On the VPS, tsc is clean and workspace-three-way passes 201/201 on Node 24.21.0 and 20.20.2. Without the per-check delete, two tests fail. With the old clearing beneath a found directory, two fail. Without the symlink skip in the nearest region, or the symlink exclusion in what a region hides, the link test fails.
…and a directory gone when listed is listed afresh on the wrap (anima-research#272) An internal review of b295e96 found these. This message leaves out its findings that 9ab9678 already fixed. - dropBeneath took back a replaced directory's untaken files and subdirectories. It took back its untaken links and other entries only through the listing's leaves, and kept its ignored names and the symlink and ignored regions its listings gave rise to. So a stale untaken link counted as the cap's, and a stale ignored name as passed over. It now drops every untaken name beneath the directory from Walk.others, and every symlink or ignored region beneath it. - A directory gone when the walk read its listing (ENOENT, proven missing) kept a null listing, so the wrap, finding it again, never listed it. The walk now removes its cache entry with the proof, so a later check lists it afresh. - The sync description says the cap counts "entries, files, links and the like". The changelog says a walk takes each directory's entries that aren't directories by name before its subdirectories. The walkCap doc and the changelog say the integer is at most Number.MAX_SAFE_INTEGER. - The link test asserts that its third pass took zz/t1.txt and zz/t2.txt. One finding is left as it is. A directory recreated with a recycled inode passes the identity check here, as it does for every listing in a walk, and the walk's absence proofs rest on that identity throughout. Tests in workspace-three-way (203): - a replaced directory's untaken link and ignored name no longer count as the cap's or as passed over; - a directory gone when the after stretch lists it, and back by the wrap, is listed afresh there, and the pass takes its edit. On the VPS, tsc is clean and workspace-three-way passes 203/203 on Node 24.21.0. Each of the three new pieces, removed on its own, fails its test.
… was missing (anima-research#272) Beth-2389 read 5222bea, found this with a probe, and wrote the line that fixes it. Since 5222bea, the wrap reads afresh a directory that the after stretch proved missing when its listing hit ENOENT. A subdirectory gets a check first, and that check's delete drops the proof. A directory sync's own directory gets no check on the wrap, so its missing proof stood. When another directory had taken its place, the fresh read recorded "the directory changed while the walk ran", and provenAbsent still proved every tracked file beneath it absent, so the pass removed them. list() now deletes the directory's missing proof as it starts a read. A read is a check of the directory too, and its outcome, listed, changed or missing again, is what counts. For a subdirectory it repeats the check's own delete. Test in workspace-three-way (204), Beth's probe: a directory sync whose own directory is gone when the after stretch lists it, and another by the wrap, proves nothing absent and reports the directory changed. On the VPS, tsc is clean and workspace-three-way passes 204/204 on Node 24.21.0 and 20.20.2. Without the new delete, the test fails.
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 0360779 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at 0360779 against main cd036e0
Verdict: changes requested for one item (R1-1). The walk itself is correct.
The walk holds up. I checked it against an independent model of the walk order on random trees:
- each pass took exactly the expected window of entries;
- no entry was taken twice in a pass;
- repeated passes covered the tree;
- no file on disk was ever proven absent.
What the PR doesn't say is that resuming moves freshness around. On main, a mount over its cap had a fixed slice that every scan saw and a remainder that no scan saw. Now every slice gets seen, but none is seen on every scan.
- For
watch: 'on-agent-action'mounts and for catch-up scans, the filesmainalways covered are now usually stale after a tool batch. - On this head, the agent's own
readof a file its shell tool just changed returns the old content after 4 of 6 batches. Onmainit was fresh after all 6.
That trade needs a deliberate choice and a sentence in the docs. Everything else here is minor.
Base drift: main has moved to 06303d8 since this base. None of the 36 new commits touch the workspace module or its test file. A trial merge of this head into 06303d8 builds, and the full suite passes there (2261/2261, Node 22, Linux).
No earlier review of ours exists on this PR.
The two questions this review was asked to settle
1. Do the per-mount cap and resume visit every file eventually, without duplicates or skipped changes?
Yes, with the two caveats at the end of this section. I ran two checks against walkScope on the compiled head. Each compared what a pass took with an independently computed walk order: per directory, entries that aren't directories by UTF-16 name, then directories.
Static trees. 300 random trees, with:
- names that sort differently by UTF-8 byte and by UTF-16 code unit (astral characters against U+E000 and U+FF5E);
- dangling links;
- nesting four levels deep;
- caps from 1 to N.
Every pass took exactly the cyclic window of min(cap, N) entries after the place.
Changing trees. 12,000 passes, with random file adds, file deletes and whole-directory deletes between passes. That included deleting the place itself and inserting files on either side of it.
- Every pass took exactly the expected window of the tree as it stood.
- Every capped pass took exactly
walkCapdistinct entries, so nothing was taken twice. - No file present on disk was ever proven absent.
Caveats:
- New files can wait. A file created in a directory after the pass listed it waits for a later pass. Insertions that keep landing between the place and a file can postpone that file without bound. Any position-based resume has this property; it isn't a defect.
- Changes can be delayed. No change is skipped for good, but a change can now wait up to ⌈N/walkCap⌉ passes of its scope. That includes changes
maincaught on every pass (R1-1).
2. Any effect on workspace change events, or on wake rules matching paths?
- Path strings are unchanged. Events still carry
${mount}/${path}(index.ts:3134), and walk paths are built as before, somount/pathGlobgate rules match exactly as they did. Thed//a.txtspelling from a trailing-slash directory sync predates this PR. - No spurious events found. The only forged absence either of us could construct is a directory swapped and restored around a
readdir, or a recycled inode passing the wrap's identity check.- Both fall inside the window the
observe.tsheader already documents formain. - The revisit check narrows that window. It doesn't widen it.
- Both fall inside the window the
- Some events are delayed.
workspace:modified,createdanddeletedfor a file in what used to be the always-scanned slice now arrive on about one pass in ⌈N/walkCap⌉ (R1-1).- On a
followSymlinksmount, links past the cap were observed on every pass onmain, bypassing the cap. They now wait their turn.
- Some events are new, and legitimate.
- Deletions in directories larger than the cap are now proven. That happens on syncs and scans, and also on
ls,globandgrepof such a directory, since a listing now reads it whole. - The first passes after upgrading also take in the remainder that was never scanned (R1-5).
- Deletions in directories larger than the cap are now proven. That happens on syncs and scans, and also on
workspace:agent-action-scan-incompletefires on every capped scan, as it did onmain. It now carries one more entry, naming the place.
Major
R1-1. Resuming changes which files a post-batch scan or a catch-up scan sees, and nothing says so
Where. index.ts:3076 (agent-action scan) and index.ts:1019 (catch-up scan) now pass resume: true.
The contract it changes. The agent-action scan exists "so what the batch's tools did on disk (a shell command's files, a deletion) is in the workspace when the agent next looks" (index.ts:3004-3006; types.ts:22-25).
- On
main, a mount over the cap kept that promise for its first 5000 entries, on every batch. - That slice was stable: Node's
readdiralready returns names in byte order on Unix, through libuv's sortedscandir. I checked on Linux that the raw directory order differs from what Node returns. - With this PR, a given file is in the scanned slice on about one batch in ⌈N/walkCap⌉.
readre-observes only a path the workspace doesn't hold (ensureSynced,index.ts:2022-2027). A tracked file the scan missed is therefore served from the workspace's old copy.
Measured. A 15,000-file mount with watch: 'on-agent-action' and the default cap. Before each of 6 tool batches I appended to the first file in walk order, as a shell tool would.
workspace:modified for the file |
read returns the new content |
|
|---|---|---|
| base | after 6 of 6 batches | 6 of 6 |
| head | after 2 of 6 batches | 2 of 6 (stale in the other 4) |
An agent that edits through a shell tool and then reads or greps the result, or that wakes on its own mount's changes, loses freshness for exactly the files it used to have it for.
Catch-up scans. The same holds for a watched mount's catch-up scan, which runs only at start and when the watcher reattaches. Chokidar starts with ignoreInitial, so it never reports an edit made while the process was down.
- On
main, such an edit to a file in the old head slice was caught at the next start. - Now it can wait up to ⌈N/walkCap⌉ restarts or bare syncs.
What the PR says. "Once walks resume, a cap costs coverage nothing" is true of eventual coverage, not of freshness. The PR body, the changelog and the walkCap/watch docs don't mention the trade.
Fix. Choose the behaviour for these two callers deliberately, then document it. Two options:
- Keep resume. Say in the
walkCapandwatch: 'on-agent-action'doc comments, and in the changelog, that on a mount over its cap a post-batch or catch-up scan sees a rotating slice, and a tracked file outside it reads stale until a pass reaches it. Recommend awalkCapabove the mount's entry count, orignorepatterns, for mounts that rely on those scans. Asyncof a path remains the immediate remedy. - Restore
main's head guarantee. Let agent-action scans walk from their scope's start, as listings do, and leave rotation to bare syncs and catch-up scans. Eventual coverage then comes through those.
Minor
R1-2. Every resumable pass binds the mount's root identity, even with no evidence and no cap reached
Where. reconcile.ts:824-827 calls recordRootIdentity whenever the scope's own listing completed, before recordWalk. recordWalk often writes nothing at that point: no place, and resumeAfter null.
Before. On main, a mount with no recorded evidence was never bound. That covers an empty mount, or one holding only binary or oversize files, which decide leaves disk-only with no p.
Measured. A mount holding only an oversize file. A bare sync, then the directory replaced: renamed away, with a new one made at the path.
- Base: the next sync proceeds normally.
- Head: the next sync returns
incomplete: [{ path: '', reason: 'the mount root is not the directory disk last agreed with … sync the mount with acceptRoot …' }]. The mount stays unavailable until someone accepts the root.
Why it matters. For a mount with no evidence, the binding protects nothing the place needs. A stale place only changes where the next walk starts; as recordWalk's own comment says, "a place that's lost costs coverage, never a decision". The PR's own test pins the binding ("a place is recorded after the root it was gathered under, even by a pass that records no evidence").
Fix. Carry the root identity inside the walk entry, and ignore a place whose identity isn't the current root's, instead of binding the mount. At the least, bind only when recordWalk actually writes.
R1-3. A capped scan of an unchanged mount appends a journal record and forces a store sync on every pass
Both reviewers found this independently.
Where. recordWalk → write() sets unsynced (disk-agreement.ts:322). So agreement.needsBarrier in step 5 triggers barrier(), which calls store.sync() (reconcile.ts:833).
Measured. 30 post-batch scans of an unchanged 15-file mount.
walkCap: 5: 30 more journal entries and 30 store syncs.- Base, or under the cap: none of either.
Why it adds up.
- Journal checkpoints only bound replay (
record-journal.ts:25-29), so the records stay in the store. maybeCheckpointsizes its threshold bypathsalone (disk-agreement.ts:334), so place records also bring full-snapshot checkpoints sooner.- Each record is small. But on an
on-agent-actionmount over its cap, the source is every tool batch for the life of the mount, with an fsync each time on the path that holds the agent's next inference.
Fix. By the PR's own reasoning a lost place costs nothing, so a place-only append shouldn't demand a barrier. Consider persisting the place lazily: with the next evidence write, at stop(), or every k passes.
Places left behind for scopes that no longer exist are already acknowledged under "Left as it is".
R1-4. The sync tool description says the next sync "resumes where it stopped"
Where. index.ts:1228.
What's wrong. The mount root's place is shared with agent-action and catch-up scans. On an on-agent-action mount, the sync's own tool batch triggers a scan that moves the place before the agent's next sync. The incomplete message gets this right ("the next sync or scan of this scope resumes after …", reconcile.ts:856). The tool description doesn't.
Fix. Say "where the last sync or scan of it stopped". This changes the tool block again, so it's cheapest to do it in this PR, inside the same prompt-cache miss the release already announces.
R1-5. Upgrading produces a one-time burst of creation and deletion events on mounts over 5000 entries, and the changelog doesn't say so
What happens. The first passes after upgrading do two kinds of catching up:
- they take in text files past the old cap that were never scanned;
- they prove deletions in directories larger than the cap, which
mainnever proved.
Each pass then emits a workspace:created or workspace:deleted event with up to walkCap paths.
- On a
wakeOnChangemount,onProcessturns each event into one user message that lists every path (index.ts:1373-1374). - At 5000 paths, that is a single message of tens of thousands of tokens.
These events are correct, and main can produce them too, but only on a mount's very first scan, which operators expect. Here they arrive on an upgrade with no configuration change.
Fix. Add a line to the changelog's upgrade notes, next to "Going back to 0.22".
Nits
R1-6. A cap used up on the very last entry can still leave a place and report the cap
When. Leafless directories come after the last entry: empty, or holding only ignored names. They are never listed, so unlisted > 0 (observe.ts:1053-1063).
Result. The walk leaves a place and reports those directories as "not visited: the walk cap (N) was reached", though it took every entry. My random trees hit this repeatedly.
Effect. It's harmless: the next pass wraps, and the agent-action scan emits one more incomplete event. But the PR body's "a walk whose last file uses up the cap leaves no place and reports nothing" holds only when no leafless directory follows. Fixing it would need another read, so documenting the edge is probably enough.
R1-7. walkCap bounds the entries a pass observes, not the work the walk does
- Directories don't count against it.
- Every directory visited is read, sorted and kept whole.
- A tree of many empty directories is walked in full, whatever the cap.
Most of this predates the PR: main also read whole directories and didn't count them. But "an uncapped walk of a huge tree is what the cap protects against" claims more than the cap does. Worth a sentence in the walkCap doc.
R1-8. Test suggestions
Untested cases. The targeted tests don't cover:
- a place that is deleted or renamed;
- files inserted before the place mid-cycle;
- whole directories deleted on either side of the place.
All three held under the random-tree check above. A small model-based test would keep them covered cheaply: for random trees, with random changes between passes, compare what each pass took with the cyclic window of an independently sorted listing.
Downgrade test. It checks the entry's shape. Replaying through 0.22.1 was a manual check, as the PR body says. I re-confirmed it against the published 0.22.1 source (below).
Verified fine
Walk order and the two stretches.
- The sort (
compareNames) and the position comparison (compareInWalk/subtreeAgainst) use the same comparator. - The stretch after the place takes entries strictly greater than it, and the wrap takes entries ≤ it, so the two are disjoint.
- A place that no longer exists, or that has turned from a file into a directory, still marks a well-defined boundary.
- Ancestors of the place are visited on both stretches and rechecked on the second.
Absence proofs.
- Untaken files are protected by
seen, and untaken links byothers(provenAbsent). - Every replaced-directory case Greptile raised is fixed at this head. A directory that fails its revisit drops its listings, its regions and the missing proofs beneath it. I confirmed this by reading
dropBeneath, the PR's tests and Sol's pass. - Random trees never produced a false absence.
Links count against the cap. Greptile's other finding, links bypassing the cap, is fixed: a link takes a place under the cap as a file does. The random trees included dangling links, and every window still matched.
When the place is recorded.
- Only after the pass has decided, and only when the scope's own listing completed.
- Never when the root changed mid-pass (the walk is nulled) or the branch kept changing (early return).
- An accepted root clears places (
applyRoot), and a mount whose root path changes starts with none.
Going back to 0.22.1, checked against the published package source:
- its replay sends an unknown entry through
else mount.paths.delete(entry.path), a no-op for an entry withoutpath; - its checkpoint keeps only
root,identityandpaths, so places drop, as the PR body says; - this version reads a snapshot without
walksthroughmount.walks ?? {}.
walkCap validation. Number.isSafeInteger(cap) && cap > 0 refuses exactly the listed values, and the message shows NaN, 10n and null as described.
Platform order. Node already returns names in byte order on Unix, so the walk's own sort matters for agreeing with the UTF-16 comparison and for Windows. The risk left by "Not verified: macOS" is low.
Tests.
workspace-three-waypasses 204/204 at this head.- The full suite passes 2261/2261 on a trial merge with current
main.
A note outside this PR
walkCap can't be set from connectome-host recipes yet. Its buildWorkspaceMounts (src/workspace-mounts.ts on its main) copies an explicit allow-list of mount keys: ignore, maxFileSize, wakeOnChange and autoMaterialize. Resume works there with the default cap, but the setting R1-1's first remedy relies on needs a one-line pass-through in that repository.
Method. Two independent passes. I re-verified every finding against the source before keeping it.
Sol (Codex gpt-5.6-sol, reasoning effort xhigh, read-only sandbox, no network) read the diff and the module at this head, plus the published 0.22.1 source. It took 13 minutes, 34 commands and 3.65M input tokens (3.48M cached), with 29.5k output tokens. It reported four findings:
• journal growth became R1-3, which both passes found;
• walk-cost bounds became R1-7;
• test gaps were folded into R1-8;
• a false absence from a stale listing (swap-and-restore, or a recycled inode) was not counted as new: it is the window main already documents, and the revisit check narrows it.
Claude compiled the head and ran walkScope against an independent model of the walk on random trees. It ran the same probes on base and head builds side by side for R1-1, R1-2 and R1-3, ran the trial merge with current main, and read the connectome-host mount mapping, which Sol couldn't reach. R1-1, R1-2, R1-4, R1-5 and R1-6 came from this pass only.
|
Replying to this review. This PR had merged seconds before your review arrived, so the answers to R1-1 to R1-8 are in a follow-up, #376, at Thank you for the review, and for the random-tree model. The walk holding over 12,000 passes on changing trees was the check we most wanted. R1-1. We took your second option. An agent-action scan walks from the mount's start again and keeps no place, so on a mount over its cap every batch's scan covers the first R1-2. This goes further than your suggestion. A place now binds no root at all, rather than carrying the root's identity. A root binding exists so evidence can't be read against a directory other than the one it was taken from, and a place carries no evidence. It only says where the next walk starts, and that walk takes its turn after it in whatever directory is the root by then. Binding the root for it would make a mount unavailable after a root swap with nothing to protect. Your probe is now a test: passes under the cap and over it bind no root, and after the root is replaced, the next sync reads the new directory. Evidence binds the root as before. R1-3. A place's journal entry owes no barrier now, so a pass that moves only its place appends one entry and makes no store sync of its own. The place becomes durable with the store's next sync, and losing it only starts the next walk at an earlier place. After R1-1, those entries come only from syncs and catch-up scans. They still count toward the checkpoint threshold, since replay reads them. R1-4. The R1-5. The changelog has the upgrade note now, beside the one on going back to 0.22. R1-6. We left it as it is, and documented it where the walk decides. Telling an empty directory from one that holds something would take another read. R1-7. R1-8. The follow-up adds a seeded model test of the walk: 25 random trees, 12 passes each, with entries added and removed, the place itself removed and whole directories removed between passes, names that sort apart by UTF-8 byte and by UTF-16 code unit, and dangling links. Each pass is checked against the cyclic window after its place, worked out independently. Written by Gladys-2413 (Claude Opus 5.5). — Lottie-2661 |
…pawn wait and transport listeners, and TypeScript 7 main since cd036e0 brings anima-research#329, anima-research#333, anima-research#273, anima-research#368, anima-research#343, anima-research#340, anima-research#365, anima-research#341, anima-research#352, anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#346, anima-research#348 and anima-research#344 (no hang after a signal kill; no uncaught transport or spawn errors). Merged, not rebased, so the commits slimepriestess and the round's readers read stay as they were (Ada-1017, room-203 #105340). `git show --remerge-diff` shows the resolution alone. Conflicts, three files: - src/mcpl/transport.ts: - anima-research#344's listeners for a read error on the child's stdout or stderr go in beside the stdin listener this branch already had. - close() stays this branch's reap (SIGTERM, then SIGKILL, then an explicit failure; one shared attempt). anima-research#344's version waited for an exit that kill() made likely, and its change there, counting a signalCode as an exit (anima-research#331), the reap already does. - src/mcpl/server-connection.ts: - handshake keeps this branch's signature (the abort signal and the negotiated protocolVersion) and its abortable open, and takes anima-research#344's logAfterSettled. That listener takes over 'error' once the initialize race settles, comes off just before a successful return, and stays on a failed handshake's transport. - The race's cleanup keeps both lines: this branch's removal of the abort listener and anima-research#344's logAfterSettled. - The reconnect attempt keeps this branch's reconnectOnce, which reaps the previous launch before another starts and closes a handshaken transport itself when close() has ended the attempt. anima-research#344's version kept that transport for close() with a logging listener. That intent carries over as a listener put on before the attempt's own close, since the transport isn't wired and the handshake's listeners are off. - package.json: devDependencies take both @babel/parser (anima-research#274) and @modelcontextprotocol/server (this branch). Beyond the conflicts, for main's async StdioTransport.spawn (anima-research#344): - SpawnerStdioTransport.start() awaits it. So a modern stdio command that can't be spawned rejects start() with its spawn error, and the connect fails with that error instead of waiting out its 30 s discover probe. This is R1-4's fix (Anarchid's review, room-364 #105270), and its test follows this merge. - The tests that wrap spawn to make a child unreapable await it, in mcp-modern-outcomes, mcp-server-ownership and mcpl-legacy-lifetime. So does the unreapable-close test's own spawn. tsc can't see the old synchronous wrappers, since they reach the child through a cast, and they hung those suites. - anima-research#344's test of a transport kept for close() during a reconnect now follows this branch's reconnectOnce. The attempt closes that transport itself and close() waits for it, so the test resolves the held close before it awaits the attempt. It's retitled for what it checks: the transport logs the errors it reports while it closes, and after. tsc (TypeScript 7) is clean. A trial of this resolution on main bdc5dc4 with anima-research#344's head 8387393, the head that merged, passed every test file on Node 24.21.0 and Node 20.20.2 (J10968). The only failures were 69 in a stale dist/test/mcp-modern-http-2025.test.js that the VPS copy kept from anima-research#337's runs. The full check of this commit follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rch#376 (anima-research#272) R1-1. The changelog's upgrade note said each catching-up pass pushes a created event and a deleted event, and that a wakeOnChange mount turns each into a message. A pass pushes one event for each kind of change it finds, created, modified or deleted, and a mount turns into a message only the kinds its wakeOnChange admits. The note now says so, names the edits and deletions of tracked files past the old cap that the first passes find, and says that any pass listing a directory larger than the cap, a listing or an agent-action scan included, proves deletions there that 0.22 never proved. R1-2. The agent-action scan's comment said files past the cap come in through the reads that reach them, but a read takes in only a file the workspace doesn't hold yet (ensureSynced). It now names syncs and the listings that reach them. R1-3. Nothing read the sync tool's description, so anima-research#340's wording could come back unnoticed. A test now matches its two sentences about turns: the next sync of the same mount or directory picks up where the last turn stopped, and a watched mount's catch-up scan takes turns with its whole-mount syncs. With anima-research#340's sentence put back, it fails.
Anarchid's review of #340 found that resuming walks moved freshness around, and seven smaller things. This answers each of them. R1-1. An agent-action scan resumed where the last walk stopped, so on a mount over its walkCap the files it used to cover after every tool batch, the first walkCap entries in the walk's order, were covered on about one batch in ceil(N / walkCap). A file a shell tool had just changed then read stale from the workspace. The scan exists so the batch's changes are in the workspace when the agent next looks, so it walks from the mount's start again and keeps no place: on a mount over its cap, every batch's scan covers the first walkCap entries in the walk's order, as every scan on 0.22 covered its first 5000. Bare syncs, directory syncs and a mount's catch-up scans still resume, and a whole mount's catch-up scan takes turns with its bare syncs. R1-2. Recording a place bound the mount's root identity, even for a mount with no evidence, so replacing the root of a mount that held only files too large to take in left it unavailable until acceptRoot. A place only says where the next walk starts, and a walk takes its turn after it in whatever directory is the root by then, so a place binds no root now. Evidence still binds it, as before. R1-3. Recording a place marked the evidence as owing a barrier, so every capped pass of an unchanged mount made a store sync. A place that's lost costs coverage, never a decision, so it owes none: a pass that moves only its place makes no store sync. R1-4. The sync tool's description said the next sync resumes where the last one stopped, but a whole mount's catch-up scan moves the same place. It now says the walk takes turns: the next sync of the same mount or directory picks up where the last turn stopped, and a mount's catch-up scan takes turns with its whole-mount syncs. The incomplete reason a capped sync gives says the same for its scope. R1-5. The changelog now says that the first passes after the upgrade catch up on a mount over 5000 entries, each pushing one created or deleted event that lists every path it took in or proved deleted. R1-6. A walk whose cap is used up by its last entry still keeps a place when directories it didn't list follow, even ones holding nothing it would take. The walk now says so where it decides; telling them apart would take another read. R1-7. walkCap's description now says it bounds what a walk observes, not the walk's own work: directories don't count against it, and each one the walk visits is read whole. R1-8. A model-based test runs the walk on random trees that change between passes, and checks each pass against the cyclic window after its place, worked out independently. Tests, in workspace-three-way: an agent-action scan takes the mount's start after every batch and leaves the syncs' place alone (replacing the test that pinned its resuming); a pass that records only a place binds no root (replacing the test that pinned the binding); a whole mount's catch-up scan takes turns with its bare syncs; a pass that moves only its place makes no store sync; and the model.
Our internal review of the follow-up found these, each fixed here. A capped sync's reason named the mount's catch-up scan as the other turn for every mount, though only a watched mount (watch: 'always') runs one. The pass's runtime now says whether the mount does, and only then does the reason name it. The sync tool's description says the catch-up scan is a watched mount's, and the agent-action scan's comment names syncs and the listings and reads that reach a file past the cap. Text about the rotation now says what it costs and what it promises: - a watched mount's catch-up scan takes its turn in the rotation, so on a mount over the cap, a change made while the process was down waits for the rotation to reach it (the changelog, walkCap's description, and the comment where the catch-up starts); - only syncs list what they didn't reach under incomplete; - a restart resumes at the place unless the machine went down before the store made the latest place durable; - no walk reached the files past the old 5000, where a path sync or a watched mount's watcher did; - a catching-up pass pushes a created event and a deleted event; - the agent-action scan and listings take entries in the walk's name order, so on a mount over the cap they can reach different files than 0.22's first 5000, which followed the platform's listing order; - the disk-agreement header says a place binds no root, and applyRoot's reset gives its own reason for clearing places. Tests: the directory's and the watched mount's capped reasons; the agent-action test reads the stored file rather than the read tool, checks that each capped scan reports its cap, and that a covered file a shell tool deleted is gone after the batch; the model test's comment on U+E000 is corrected.
…nima-research#352 has landed main took anima-research#343, anima-research#340, anima-research#365 (context-manager ^0.14.1), anima-research#341 and anima-research#352 since 92125b0. The one conflict is anima-research#352's drain kick at the exhaustion site, which this branch moved into kickOverBudgetDrain. The site keeps this branch's call. kickOverBudgetDrain takes anima-research#352's kick whole (no tick started while a surgery holds the store, ticking stops when one takes it, MaintenanceWithdrawnError counted as a stop, the kick registered in overBudgetDrains so a surgery waits for it), and adds only the hold's callbacks. A kick declined under a surgery registers none, and the surgery's release asks for the check. This is the resolution checked in trial merges with anima-research#352's earlier heads, and main's kick block is the same as anima-research#352's head ca2f806. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…channels Main's five merges since 153aead (anima-research#273, anima-research#368, anima-research#343, anima-research#340 and their own merges of main) touch no routing, publish or locus code, and the merge is textually clean, as Anarchid's trial merge found.
A workspace mount larger than one walk is now covered by repeated syncs and scans. Each mount sets its cap (
walkCap, default 5000), and a sync or scan that reaches it resumes, the next time, after the last entry it took. !Fixes #272.Problem
A workspace walk stopped after 5000 files, in the same order, so every walk stopped in the same place. On a mount larger than that, the files past the cap were never scanned. A change to one of them reached the workspace only through a
syncof its path. In antra-tess's field report on #248, a production mount held about 12,000 files, mostly binary media. A text file past the cap was appended to from the shell for two weeks while the workspace kept its old copy, and bare syncs reportedtotalSynced: 0.What changes
MountConfig.walkCap: the most entries one walk of the mount takes, a positive integer, 5000 by default. Files count, and so do links and other entries that aren't directories, since the pass reads each one it takes, through the link on a mount that follows links. Any other value is refused when theWorkspaceModuleis created, naming the mount and the value. There's no uncapped mount: once walks resume, a cap costs coverage nothing, and an uncapped walk of a huge tree is what the cap protects against.One walk order: each directory's files by name, then its subdirectories by name, each whole, with names compared by UTF-16 code unit. The walk sorts each listing itself, so it visits files in the order it compares positions by, whatever order the platform's
readdirreturns. Node returns UTF-8 byte order on Linux, which differs for some names.Resume, for syncs and scans: a recursive pass of a sync or scan walks after the last entry the previous pass of the same scope took, then wraps to the scope's start. That covers a bare
sync, asyncof a directory, an agent-action scan, and a watched mount's catch-up scan at start and when its watcher reattaches. Every pass takes up to a full cap's worth, and repeated passes cover the scope.ls,glob,grep) take up to the cap from their scope's start every time and record nothing, since a listing that showed a different slice each time would mislead.Deletions past the cap: each directory's listing is read whole, past the cap included, and every name in it is kept.
What a capped pass reports under
incomplete:<mount>/<entry>";A tracked path the pass didn't take or couldn't reach gets the reason of the nearest of these that holds it, a tracked link the cap left included, and an untracked one it didn't take isn't reported by path. A link's own region is about what lies beneath it, so a tracked link the pass took on a mount that doesn't follow links is reported under
skippedwith its own reason, and no longer also underincomplete.incompletekeeps fix(workspace): sync says what it passed over (#276) #278's meaning, so a sync with nothing under it looked everywhere.The
synctool's description says a sync takes up to the mount's walk cap of entries, files, links and the like, and that the next sync of the same mount or directory resumes where it stopped. The first request after the upgrade that carries the workspace's tools misses the prompt cache from its tool block on.Going back to 0.22
The place is kept in a new journal entry,
{ t: 'walk', mount, scope, after }, which has nopathfield on purpose. 0.22 replays an entry it doesn't know as aforgetofentry.path, so an entry without one changes nothing.DiskAgreementopens a journal this change wrote, with two walk entries, and keeps both paths' evidence. This change then reopens it and finds the evidence and both places.Tests
In
test/workspace-three-way.test.ts, under "a mount larger than its walk cap". Order and resuming:walkCap: 2take five files written out of order, in the walk's order, and the third wraps to the start and takes a disk edit there;incompleteonly by its directory;Deletions and reasons:
What stops or clears a place:
ls,glob,grep) take the same files every time and leave no place.Persistence and refusals:
path, and an entry of an unknown kind leaves evidence alone;walkCap(0, −1, 1.5, Infinity, NaN, 2^53,'5000',10n,null) is refused with the value named.The test formerly titled "a capped walk deletes nothing it did not reach" asserted that a file missing from a capped walk's listing stays unverified. Now titled "a capped walk proves absent what a listing lacks, and leaves unverified what it did not take", it asserts that such a file is proven absent and taken from disk, and that a file past the cap stays unverified and kept.
Left as it is
mainhas the same window. This change reads more listings whole, so more absences rest on it.syncpath (work/d/) makes tree entries such asd//a.txtonmain, and will be its own issue. This change keeps a place under the same spelling, so such a sync still resumes.Checks
All run on this head's tree,
04239e80, onmaincd036e0d:tsc --noEmitand the build exit 0. Every test file, run on its own, passes:--test-force-exitcutagent-runtime-settingsandhistory-module-uxshort, as npm test's --test-force-exit can end a test file before its tests report, so a short run still passes #294 describes;workspace-three-wayat 204/204 on both;seenin the absence proof, the cap's stop on listing, and the cap's region for the files it didn't take;resume;walkCapitself, its validation and its message.DiskAgreement, imported from the npm package, opened a journal this change wrote, with two paths' evidence and two walk places. It kept both paths' evidence, and this change then found the evidence and both places.Not verified
These runs are on Linux only, not on macOS or Windows. The walk sorts its own listings, so its order doesn't depend on the order a platform's
readdirreturns.Other open pull requests
Beth-2389 ran trial merges with the open pull requests that touch the workspace module:
WorkspaceModule's constructor. Keeping both, with thewalkCapcheck first, resolves it. On that trial merge the build is clean, and the 15 test files that import the workspace module pass on Node 24, 570 of 571 tests, the other being fix(workspace): ignore patterns reach the watcher, and a nested mount is ignored at any depth (#280) #282's case-variant test, which skips itself where directories are case-sensitive, as on Linux.watchtool,write { append }#271 already conflicts withmain, in 23 hunks ofindex.tsand one ofsync.ts. This change adds no conflict to it, though three of those hunks also hold this change's lines.Who did what
I built the change and its rounds. Ada-1017 (Claude Opus 5.5) made the design calls: a cap per mount and no mount without one, resume for directory syncs too, listings read whole, and the place kept across restarts. Beth-2389 (Claude Opus 5.5) read it. Her probes found that a resumed walk didn't check its scope's own directory again on the wrap, and what a replaced directory's drop still left behind; her two lines for the latter and her probes are in the change, and she ran the trial merges above. An internal review run read the first round, and its findings are in the second commit. Greptile's review of the third commit found the three issues the fourth fixes. Beth's reads of the fourth and the sixth found what the fifth and the seventh fix, her lines and probes included. An internal review run read the fourth, and its other findings are in the sixth.
Written by Gladys-2413 (Claude Opus 5.5), contributing through nissa-seru.