fix(rules): terminate findProjectRoot walk at non-cwd drive roots

Merged
LA
1mo ago
+36-1

Summary

On Windows, findProjectRoot loops forever when the target path is on a different drive than the process cwd (or on a UNC share) and no project marker exists up-tree. This is a synchronous infinite loop, so it freezes the host agent's entire event loop at 100% CPU.

Root cause

const filesystemRoot = resolve("/"); // -> cwd's drive root, e.g. "E:\"
while (true) {
    // ... marker checks ...
    if (currentDirectory === filesystemRoot) return null; // "C:\" === "E:\" never holds
    currentDirectory = dirname(currentDirectory);          // dirname("C:\") === "C:\"
}

The walk terminates only when it reaches resolve("/"), which is cwd's drive root. For a target on another drive, dirname() of that drive's root returns the root itself, so the equality never holds and the loop never exits.

Real-world impact (how this was found)

Discovered while debugging frozen sessions in @code-yeongyu/senpi, which vendors pi-rules as a builtin extension. On a Windows machine with the workspace on E:, every read/edit/write tool call targeting a file on C: (temp screenshots, files under the user profile, etc.) froze the session permanently:

  • 6/6 cross-drive tool calls hung; 26/26 same-drive calls succeeded (session JSONL audit)
  • Hung process burned one full core (53.6s CPU in 60s)
  • 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). This preserves the existing resolve("/") early-exit and covers cross-drive roots and UNC share roots uniformly.

Tests

  • New regression test exercising the real cross-drive scenario on multi-drive Windows machines (no-ops on POSIX and single-drive Windows where the scenario cannot occur; pre-fix it hangs, post-fix it returns null instantly).
  • Verified live on Windows 11 (cwd on E:, temp on C:): the new test passes in ~1ms with the fix.
  • Full suite: 244 tests, same 10 failures as the base commit (pre-existing Windows symlink-privilege environment issues in scanner/finder/engine/extension-registration/tool-paths tests); no new failures.

Downstream

senpi vendors this package (packages/coding-agent/src/core/extensions/builtin/rules, MANUAL_PACKAGES). A companion senpi PR applying the same guard to the vendored copy + changes.md entry will follow.


Summary by cubic

Fixes a Windows-only infinite loop in findProjectRoot that froze the agent on cross‑drive or UNC paths without markers. The walk now terminates correctly, preventing 100% CPU hangs on these tool calls.

  • Bug Fixes
    • Stop traversal when dirname() stops changing the path instead of relying only on resolve("/"), covering cross-drive and UNC roots.
    • Add a Windows multi-drive regression test that returns null instead of hanging.