Skip to content

feat(local-mount)!: async createMount yields the event loop during init#105

Merged
willwashburn merged 3 commits into
mainfrom
fix/local-mount-async-create-mount
May 8, 2026
Merged

feat(local-mount)!: async createMount yields the event loop during init#105
willwashburn merged 3 commits into
mainfrom
fix/local-mount-async-create-mount

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

Breaking change

Callers must await createMount(...). There is no createMountSync. The package is pre-1.0; the only known external consumer is the AgentWorkforce CLI, which already invokes the function from an async context. launchOnMount awaits internally, so its surface is unchanged.

Implementation note

Yields use await new Promise<void>((resolve) => setImmediate(resolve)) — the same pattern already used inside syncBack for abort-cooperative scans. One yield at each recursion entry plus one every 64 file entries within a single directory keeps both deeply-nested and very-flat trees responsive without measurable perf cost (the issue calls one-yield-per-directory ""enough"").

Test plan

  • npm test in packages/local-mount — 31 passed (4 files).
  • npm run typecheck in packages/local-mount — clean.
  • New regression test createMount > yields the event loop during init so consumer setInterval can fire drives a 5ms setInterval counter while createMount processes 5000 files across 50 directories and asserts the counter advanced ≥ 2 ticks. A sync walker would leave it at 0.

🤖 Generated with Claude Code

Closes #104.

createMount used to walk the project tree synchronously via readdirSync +
copyFileSync, freezing the consumer's event loop for the entire init window.
A consumer-side setInterval (e.g. an `ora` spinner) would render its first
frame and then visibly hang until createMount returned.

Convert createMount to `Promise<MountHandle>` and have walkProjectTree yield
between directory entries — once at the start of each recursion and once per
64 entries inside a directory to handle large flat trees. The goal is consumer
ergonomics, not throughput; #102 still owns the perf push (hardlinks, parallel
copy, fewer syscalls).

Breaking: callers must `await createMount(...)`. The package is pre-1.0 and
the only known consumer is the AgentWorkforce CLI, which already wraps the
call in an async function. `launchOnMount` awaits internally so its surface
is unchanged.

Tests: existing mount/auto-sync/launch suites updated to await; new
regression drives a 5ms setInterval counter while createMount processes 5000
files across 50 dirs and asserts the counter advanced — a sync walker would
leave it at 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7da6d4ef-8430-457c-b640-912ccec63e70

📥 Commits

Reviewing files that changed from the base of the PR and between 600495e and edfe7e4.

📒 Files selected for processing (1)
  • packages/local-mount/src/launch.ts

📝 Walkthrough

Walkthrough

This PR converts createMount to an async function that yields to the event loop during directory traversal (setImmediate yields, initial and periodic every 64 entries). Tests, launch integration, README, CHANGELOG, and trajectory records are updated accordingly.

Changes

Async createMount with Event-Loop Yielding

Layer / File(s) Summary
API Signature
packages/local-mount/src/mount.ts
createMount becomes async and now returns Promise<MountHandle>.
Async Walker Implementation
packages/local-mount/src/mount.ts
walkProjectTree converted to async; mount-tree copy is awaited; new WALK_YIELD_EVERY and yieldToEventLoop() use setImmediate to yield at directory entry and periodically.
Consumer Integration
packages/local-mount/src/launch.ts, packages/local-mount/src/launch.test.ts, packages/local-mount/src/auto-sync.test.ts
launchOnMount now awaits createMount; finalize guards missing handle; spawn cwd cached; launch test mock changed to mockResolvedValue; auto-sync tests await createMount in setup.
Mount Test Suite
packages/local-mount/src/mount.test.ts
All tests converted to async/await; validation tests updated to use await expect(...).rejects.toThrow(...); new regression test asserts setInterval ticks advance during large-tree mount.
Documentation
packages/local-mount/README.md, packages/local-mount/CHANGELOG.md
README documents async return type and yielding behavior; CHANGELOG records breaking API change and removal of createMountSync.
Trajectory Records
.trajectories/completed/2026-05/*, .trajectories/index.json
New trajectory JSON/MD and index entry record completion of issue #104, decisions (async createMount, setImmediate yielding strategy), timestamps, agents, and retrospective.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant createMount
  participant walkProjectTree
  participant EventLoop
  CLI->>createMount: await createMount(projectDir, mountDir)
  createMount->>walkProjectTree: await walkProjectTree(root)
  walkProjectTree->>EventLoop: setImmediate yield (initial + periodic)
  EventLoop-->>walkProjectTree: resume traversal
  walkProjectTree-->>createMount: traversal complete
  createMount-->>CLI: resolve(MountHandle)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through files, then gently bowed,
I taught the walker to yield and allow,
setImmediate whispers, spinners can play,
Promises kept, the mount wakes OK,
A tiny rabbit cheers the event loop today.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: converting createMount to async and making the walker yield the event loop during initialization.
Description check ✅ Passed The description is well-related to the changeset, explaining the motivation, implementation details, breaking changes, and test plan for making createMount async with event-loop yielding.
Linked Issues check ✅ Passed The PR fully addresses issue #104 by converting createMount to async returning Promise, adding event-loop yields via setImmediate between directory entries and every 64 entries within directories, updating all tests to await the async API, and adding a regression test confirming event-loop yielding.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of making createMount async and yielding the event loop during init. No unrelated changes to other systems or out-of-scope modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/local-mount-async-create-mount

Comment @coderabbitai help to get the list of available commands and usage tips.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/local-mount/src/launch.ts (1)

69-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move await createMount(...) inside the guarded lifecycle path.

Right now createMount runs before the try/finalize flow. If mount creation fails mid-initialization, cleanup is skipped and temporary mount state can be left behind.

Suggested refactor
 export async function launchOnMount(opts: LaunchOnMountOptions): Promise<LaunchOnMountResult> {
-  const handle: MountHandle = await createMount(opts.projectDir, opts.mountDir, {
-    ignoredPatterns: opts.ignoredPatterns ?? [],
-    readonlyPatterns: opts.readonlyPatterns ?? [],
-    excludeDirs: opts.excludeDirs ?? [],
-    agentName: opts.agentName,
-    includeGit: opts.includeGit,
-  });
+  let handle: MountHandle | undefined;
 
   let syncedCount = 0;
   let finalized = false;
   let autoSync: AutoSyncHandle | undefined;
 
   const finalize = async (): Promise<void> => {
-    if (finalized) return;
+    if (finalized || !handle) return;
     finalized = true;
     try {
       let autoSyncChanges = 0;
       if (autoSync) {
         await autoSync.stop({ signal: opts.shutdownSignal });
@@
-      const finalSynced = await handle.syncBack({ signal: opts.shutdownSignal });
+      const finalSynced = await handle.syncBack({ signal: opts.shutdownSignal });
       syncedCount = autoSyncChanges + finalSynced;
       if (opts.onAfterSync) {
         await opts.onAfterSync(syncedCount);
       }
     } finally {
       handle.cleanup();
     }
   };
 
   try {
+    handle = await createMount(opts.projectDir, opts.mountDir, {
+      ignoredPatterns: opts.ignoredPatterns ?? [],
+      readonlyPatterns: opts.readonlyPatterns ?? [],
+      excludeDirs: opts.excludeDirs ?? [],
+      agentName: opts.agentName,
+      includeGit: opts.includeGit,
+    });
+
     if (opts.onBeforeLaunch) {
       await opts.onBeforeLaunch(handle.mountDir);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-mount/src/launch.ts` around lines 69 - 77, The call to
createMount is executed before entering the guarded lifecycle, so failures can
skip cleanup; move the await createMount(...) into the protected try/finalize
flow inside launchOnMount so any error during mount initialization triggers the
existing cleanup/finalize logic. Specifically, open the try block in
launchOnMount and perform const handle: MountHandle = await createMount(...)
there (using the same options object), ensuring the finalize/cleanup path still
runs if createMount throws; keep all existing references to handle, finalize,
and MountHandle intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-mount/README.md`:
- Line 30: The README wording overstates the yield cadence: update the
description for createMount / walker (and MountHandle) to say the walker yields
periodically during traversal (e.g., at directory entries and additionally every
64 entries) rather than “between directory entries”; change the phrase “between
directory entries” to something like “periodically during traversal (yields at
entries and every 64 entries)” so it matches the actual implementation.

---

Outside diff comments:
In `@packages/local-mount/src/launch.ts`:
- Around line 69-77: The call to createMount is executed before entering the
guarded lifecycle, so failures can skip cleanup; move the await createMount(...)
into the protected try/finalize flow inside launchOnMount so any error during
mount initialization triggers the existing cleanup/finalize logic. Specifically,
open the try block in launchOnMount and perform const handle: MountHandle =
await createMount(...) there (using the same options object), ensuring the
finalize/cleanup path still runs if createMount throws; keep all existing
references to handle, finalize, and MountHandle intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 824c3c1a-6cc3-4a5a-a16b-8b2f140a3fe2

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb479c and 44f23d7.

📒 Files selected for processing (10)
  • .trajectories/completed/2026-05/traj_v1un6n66y38i.json
  • .trajectories/completed/2026-05/traj_v1un6n66y38i.md
  • .trajectories/index.json
  • packages/local-mount/CHANGELOG.md
  • packages/local-mount/README.md
  • packages/local-mount/src/auto-sync.test.ts
  • packages/local-mount/src/launch.test.ts
  • packages/local-mount/src/launch.ts
  • packages/local-mount/src/mount.test.ts
  • packages/local-mount/src/mount.ts

}
```

`createMount` returns `Promise<MountHandle>`. The walker yields the event loop between directory entries so consumer-side timers (e.g. an `ora` spinner driven by `setInterval`) keep firing while the mount is being built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the yield cadence wording to match implementation.

The text says the walker yields “between directory entries,” but the implementation yields at directory entry plus every 64 entries. Consider wording this as “periodically during traversal” to avoid overpromising per-entry yielding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-mount/README.md` at line 30, The README wording overstates the
yield cadence: update the description for createMount / walker (and MountHandle)
to say the walker yields periodically during traversal (e.g., at directory
entries and additionally every 64 entries) rather than “between directory
entries”; change the phrase “between directory entries” to something like
“periodically during traversal (yields at entries and every 64 entries)” so it
matches the actual implementation.

willwashburn and others added 2 commits May 8, 2026 13:37
Resolved conflicts in packages/local-mount/src/mount.ts and
packages/local-mount/src/mount.test.ts:

- mount.ts walkProjectTree: keep `await` on the recursive call from this
  branch; adopt main's expanded signature (`currentMountDir`,
  `excludeRules`, `safeMountDir`) from #102 so the perf changes and the
  event-loop yields coexist.
- mount.test.ts: keep main's broader test name "common cache and build
  output paths" but mark it `async` and `await createMount`. Convert the
  two new tests main added (`can opt out of broad default excludes…`,
  `syncBack: skips files under default excluded paths`) to await
  createMount as well.

vitest: 33 passed (4 files), including the #104 event-loop yield
regression and the #102 default-excludes coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y block

createMount has side effects before it can throw — it mkdirs the mount
directory, writes the marker file, then walks the project tree with
copyFileSync calls that can fail (permission, ENOSPC, partial reads). With
the call sitting before the try/catch+finalize block, a failure mid-init
left a partial mount on disk and skipped the finalize lifecycle.

Hoist `let handle: MountHandle | undefined` above finalize and move the
createMount call to the top of the try block so any error during mount
initialization triggers the existing finalize path. Guard finalize against
an undefined handle (createMount itself does not currently self-clean its
partial mount, so finalize harmlessly returns when there's no handle to
sync or cleanup) and capture handle.mountDir as a local const for the
spawn closure to satisfy TypeScript's narrowing across the closure
boundary.

All 33 vitest cases still pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit d3dce6b into main May 8, 2026
6 of 7 checks passed
@willwashburn willwashburn deleted the fix/local-mount-async-create-mount branch May 8, 2026 17:49
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.

createMount blocks the consumer's event loop during init

1 participant