Skip to content

fix(daemon): do not stop on a file Windows reports as access denied; fix the graph.json Windows flake - #212

Merged
taylorg009 merged 3 commits into
mainfrom
fix-windows-persist-rename
Oct 21, 2026
Merged

taylorg009 merged 3 commits into
mainfrom
fix-windows-persist-rename

Conversation

@taylorg009

@taylorg009 taylorg009 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Two Windows problems found while chasing a windows-release flake in cgraph_daemon_server_test ("incremental state re-persisted to graph.json"). Evidence comes from a diagnostic branch (#211, to be closed) that repeated the test on the Windows runners with timing and Restart Manager logging.

1. The daemon stopped on a file Windows reports as access denied (product bug)

std::filesystem::exists(path) throws when the path cannot be stat'ed, and Windows reports a file that is being replaced (an editor's save) as "Access is denied". Four call sites used the throwing form:

All four now use the error_code overload. A file that cannot be checked counts as absent for that pass, as the neighbouring file_size/last_write_time checks already treat it, and the watcher reports it again once it is written.

Tests: file_cache_test and incremental_update_test reproduce the throw on POSIX with an unsearchable parent directory (exists throws status: Permission denied there). Both fail without the source change and pass with it.

2. The flake itself was the test starving the daemon's replace (test only)

try_write_file_atomically already retries MoveFileExW for 10 s while the target is held open. #211 showed the replace denied for longer than that, and Restart Manager named only the test process as the holder. The test polled graph.json and the stat index with std::ifstream every 20 ms, and MSVC opens without FILE_SHARE_DELETE. The test now reads those files through read_shared, which on Windows opens with FILE_SHARE_DELETE so the replace goes through mid-read. POSIX is unchanged.

Verification

  • Local (Linux, default preset): 104/104 tests; cgraph_daemon_server_test 3 repeats green.
  • Windows, diag: Windows persist retry timing (do not merge) #211 (same two changes, run 39527312675 at 865eab3): cgraph_daemon_server_test passed 40/40 on windows-default and 40/40 on windows-release, with 0 denied replaces. Before the changes, the same harness failed on windows-release in both rounds (round 1 also hung on windows-default when the daemon stopped on exists: Access is denied).
  • CI on this PR (30c58fe): 12/12 green, including both Windows jobs.
  • Review follow-up (30c58fe): a failed exists() now classifies as Unreadable, not Deleted, so a full rescan keeps a verified file's nodes. incremental_update_test indexes the file before locking it; a mutant that erases indexed files on a failed check fails it.

🤖 Generated with Claude Code

taylorg009 and others added 2 commits October 20, 2026 18:20
std::filesystem::exists(path) throws when the path cannot be stat'ed.
Windows reports a file that an editor is replacing as "Access is
denied", so the enrichment walk (classify_cached_file,
is_valid_cached_record) and the watcher's incremental update threw, and
the daemon stopped: exit 1 since #179, std::terminate before. Seen on
windows-default CI as "enrichment refresh failed: ... exists: Access is
denied ... docs\guide.md".

Use the error_code overloads. A file that cannot be checked counts as
absent for this pass, as the neighbouring file_size/last_write_time
checks already treat it; the watcher reports it again once written.
Regression tests reproduce the throw on POSIX with an unsearchable
parent directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dows

daemon_server_test polled graph.json and the stat index with std::ifstream
every 20 ms. MSVC opens without FILE_SHARE_DELETE, so the daemon's
MoveFileExW replace failed with access denied for its whole 10 s retry
window and the test gave up (windows-release flake "incremental state
re-persisted to graph.json"). Restart Manager named the test process as
the only holder. read_shared opens with FILE_SHARE_DELETE on Windows, so
the replace goes through while a read is in flight; POSIX is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

PR brief: 0 agent commits · 12 of 110 test files reach this diff

Run at least these. Selection is a safe superset; it never marks a test safe to skip.

Summary PR #212 at 30c58fe
Changed 6 files: 6 mapped to 7 symbols
Tests reached 12 of 110 ▮▮▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯▯ 11% of the suite
Downstream code 5 files, 41 dependents
Intent no checkpoints on this branch (3 commits)
Since push 903a881 1 new commit, 0 changed files, 0 tests reached
Compared against base d0cbc70

What each commit did

Commit Intent Files Reaches Ran before push
d0d9154 fix(daemon): do not stop on a file Windows reports as acces… no checkpoint 5 files 5 files, 12 tests
903a881 test(daemon): read daemon-written files with FILE_SHARE_DEL… no checkpoint 1 file 0 files, 1 test
30c58fe fix(file-cache): a failed exists() is Unreadable, not Delet… no checkpoint 4 files 5 files, 12 tests

Claims checked

no checkpoint on this branch makes a claim the graph can check

What each changed file reaches

Changed file Symbols touched Reaches Tests
file_cache.cpp classify_cached_file 5 files 12
semantic_chunk_plan.cpp is_valid_cached_record 4 files 7
incremental_update.cpp apply_incremental_code_updates 2 files 6
tests/smoke/daemon_server_test.cpp test code, selected directly 1
tests/smoke/file_cache_test.cpp test code, selected directly 1
tests/smoke/incremental_update_test.cpp test code, selected directly 1

Paths above are under src/engine/ unless shown in full.

Tests to run (12)

Blast radius by changed file: 41 dependents

blastline 0.13.0. Selection is advisory unless your workflow gates on it. How selection works. Graph 307ba96. Intent: 0 of 3 commits. Not checked: symbol changes: no --change-context given.

Review of #212: classify_cached_file returned Deleted when exists() failed,
so a full stat rescan that met a file mid-save on Windows dropped that
file's nodes until the next edit. Unreadable makes every caller keep the
last verified entry (full_stat_index_rescan refuses, the incremental path
and the chunk planner skip). A clean not-found is still Deleted.

incremental_update_test now indexes the file before locking it and checks
its nodes survive the unreadable Modified event; a mutant that erases
indexed files on a failed check fails it. Drop the redundant <windows.h>
block in daemon_server_test (unreadable_file.hpp already includes it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@tgod009 tgod009 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review: NON-BLOCKING, ready to merge.

  • All four exists() calls on the enrichment-worker and serve-loop paths use the error_code overload; the remaining throwing calls on those paths touch only the project root, never a file being saved.
  • Incremental path: an indexed file whose check fails is kept and republished, not erased; a never-indexed file is skipped. The watcher re-reports it when its token changes.
  • The review's main SHOULD (a full rescan dropping a file whose check failed) is fixed in 30c58fe (Unreadable keeps the verified entry), and the incremental test now covers the indexed-file branch (a mutant that erases fails it).
  • read_shared closes its handle before returning, so removing persisted.close() is safe.

Windows evidence: #211 ran cgraph_daemon_server_test 40/40 on each Windows job with 0 denied replaces; CI on 30c58fe is 12/12 green.

@taylorg009
taylorg009 merged commit a8b2c1a into main Oct 21, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants