Skip to content

fix(coding-agent): terminate vendored rules findProjectRoot walk at non-cwd drive roots - #432

Merged
code-yeongyu merged 3 commits into
code-yeongyu:mainfrom
lavi-kj:fix/rules-find-project-root-cross-drive-walk
Jul 30, 2026
Merged

code-yeongyu merged 3 commits into
code-yeongyu:mainfrom
lavi-kj:fix/rules-find-project-root-cross-drive-walk

Conversation

@lavi-kj

@lavi-kj lavi-kj commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On Windows, every read/edit/write tool call targeting a file on a different drive than the session cwd freezes the agent session permanently: the vendored pi-rules findProjectRoot walks parent directories looking for project markers and never terminates, spinning one core at 100% inside the rules extension's tool_result handler.

Root cause

findProjectRoot terminates its walk only when currentDirectory === resolve("/"), but resolve("/") is cwd's drive root (e.g. E:\), while dirname() of another drive's root returns that root itself (e.g. C:\). For cross-drive (or UNC) targets with no marker up-tree, the exit condition never holds and the synchronous loop never ends.

How this was found (evidence)

Reported as repeatedly freezing interactive sessions. Reproduced and dissected on Windows 11 with senpi 2026.7.26, workspace on E::

  • Session JSONL audit: 6/6 tool calls targeting C: paths (temp paste images, files under the user profile) never produced a toolResult; 26/26 same-drive calls succeeded.
  • A headless senpi --print repro burned 53.6s CPU in 60s before producing any output.
  • V8 inspector stack of the hung process:
#0 findProjectRoot (rules/rules/project-root.js)
#1 fingerprintDynamicTargets (rules/rules/engine.js)
#2 tool_result handler (rules/index.js)
#3 emitToolResult (extension runner)

Because the loop is synchronous, the abort signal is never processed: esc cannot interrupt and users must kill the process.

Fix

Stop the walk when dirname() stops progressing (parentDirectory === currentDirectory). The existing resolve("/") early-exit is preserved; cross-drive and UNC roots are covered uniformly.

  • packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts — the guard (vendored file; adaptation documented in the extension's changes.md per the contract)
  • test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts — regression test exercising the real cross-drive scenario on multi-drive Windows machines (no-ops where the scenario cannot occur; pre-fix it hangs, post-fix returns null instantly)

Upstream

The same fix was submitted to the vendored source: code-yeongyu/pi-rules#19 — the changes.md entry notes this adaptation can be dropped once a fixed pi-rules release is re-vendored.

Validation

  • New regression test passes on Windows 11 (cwd on E:, temp on C: — real cross-drive), ~5ms.
  • Root npm run check clean (biome + tsgo + lock/import checks).
  • The identical fix in the pi-rules repo passes its full project-root suite 12/12 with no new failures.

Summary by cubic

Fixes a Windows-only infinite loop in findProjectRoot when accessing files on a different drive than the cwd. The walk now terminates when dirname() stops progressing and returns null for cross-drive and UNC paths without markers.

  • Bug Fixes
    • Updated packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts to stop the walk when dirname() no longer changes the path (keeps the resolve("/") early exit).
    • Added packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts to exercise the cross-drive case in CI and assert it returns null instead of hanging.
    • Documented the vendored change in packages/coding-agent/src/core/extensions/builtin/rules/changes.md; upstream fix submitted: fix(rules): terminate findProjectRoot walk at non-cwd drive roots pi-rules#19.

Written for commit 558123a. Summary will update on new commits.

Review in cubic

lavi-kj and others added 3 commits July 28, 2026 20:16
…on-cwd drive roots

On Windows, resolve("/") is the cwd drive's root, but dirname() of
another drive's root returns itself. For targets on a different drive
than cwd (or UNC shares), the vendored pi-rules findProjectRoot marker
walk never reaches filesystemRoot and spins forever - a synchronous
infinite loop in the rules extension's tool_result handler that freezes
the agent session (100% CPU, abort signal never processed) on every
read/edit/write call targeting such paths.

Stop the walk when dirname() stops progressing, document the adaptation
in the extension's changes.md, and add a cross-drive regression test.

Upstream fix for the vendored source: code-yeongyu/pi-rules#19

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

VERDICT: MERGE_OK

The production fix is small, correctly placed, and directly addresses the reported synchronous infinite loop. findProjectRoot now treats a dirname() fixpoint as a filesystem root, so a non-cwd Windows drive root or UNC root terminates instead of spinning forever (packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts:28-35). The existing cwd-root check remains first (project-root.ts:24-26), so POSIX / behavior is unchanged. The vendored divergence and upstream removal condition are also documented clearly (packages/coding-agent/src/core/extensions/builtin/rules/changes.md:10).

I found one serious test-quality defect during review: the original regression returned immediately on non-Windows and on single-drive Windows, so its body ran in none of the standard GitHub-hosted CI environments. Mutation-checking confirmed that the original test still passed after reverting the production guard. I fixed and pushed this as 558123aa3:

  • The test now simulates an E:\ cwd root and a C:\workspace\plain\nested target with the real path.win32 operations (packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts:5-23).
  • It exercises the real findProjectRoot implementation on every platform and asserts the Windows drive-root fixpoint plus the expected null result (0000-rules-find-project-root-cross-drive.test.ts:25-35).
  • With the production guard temporarily reverted, this deterministic test entered the old synchronous loop and hit the bounded 5-second test-process timeout. Restoring the guard made it pass in 263 ms.

Verification:

  • npm test -- test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts from packages/coding-agent: 1 file / 1 test passed
  • Mutation check with project-root.ts guard reverted: timed out after 5 seconds, reproducing the infinite loop; source was restored before commit
  • POSIX invariant: path.posix.dirname("/") === "/"
  • Root npm run check: passed (Biome, dependency/import/lock checks, tsgo, browser smoke, web UI check)
  • Branch is current with origin/main (git rev-list --left-right --count HEAD...origin/main = 3 0); no additional main merge was required

No remaining blocking findings. The core change is appropriately minimal, the vendored adaptation is documented, and the regression now genuinely protects the behavior in CI.

@code-yeongyu
code-yeongyu enabled auto-merge July 30, 2026 02:52
@code-yeongyu
code-yeongyu merged commit a26c373 into code-yeongyu:main Jul 30, 2026
9 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