Skip to content

Fix local-mount syncBack full-tree walk after healthy autosync#138

Merged
khaliqgant merged 2 commits into
mainfrom
fix-134-dirty-syncback
May 11, 2026
Merged

Fix local-mount syncBack full-tree walk after healthy autosync#138
khaliqgant merged 2 commits into
mainfrom
fix-134-dirty-syncback

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • add auto-sync watcher health, pending flush, and mount-side dirty path APIs
  • let syncBack accept explicit mount-relative paths while keeping the default full-walk fallback
  • use the dirty path fast path from launchOnMount only when auto-sync watcher state stayed healthy

Closes #134.

Verification

  • npm run typecheck --workspace=packages/local-mount
  • npm test --workspace=packages/local-mount
  • npm run build --workspace=packages/local-mount

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Auto-sync now tracks mount-side dirty paths, exposes flushPending/getDirtyPaths/watchersHealthy, widens MountHandle.syncBack to accept optional path filters, and makes launchOnMount gate fast-path final sync-back on watcher readiness—falling back to a full sync when watchers are degraded.

Changes

Auto-sync dirty-path tracking and sync-back optimization

Layer / File(s) Summary
Public API contract updates
packages/local-mount/src/auto-sync.ts, packages/local-mount/src/mount.ts, packages/local-mount/README.md
AutoSyncHandle expands with flushPending(), getDirtyPaths(), watchersHealthy(). README documents syncBack({ paths }) and auto-sync diagnostics.
Stop settle constant
packages/local-mount/src/auto-sync.ts
Introduce STOP_EVENT_SETTLE_MS to optionally delay teardown after healthy watcher init.
Auto-sync internal state and watcher scheduling
packages/local-mount/src/auto-sync.ts
Add pendingPaths, dirtyMountPaths, watchersReadySettled/watcherDegraded, stopping, and scheduling/debounce helpers to normalize and coalesce per-path events.
Reconcile completion tracking
packages/local-mount/src/auto-sync.ts
runReconcile distinguishes completed vs aborted runs and clears pending/dirty queues only on successful completion; totalChanges updated from reconcile result.
flushPending implementation
packages/local-mount/src/auto-sync.ts
flushPending() drains queued paths incrementally when watchers are healthy or falls back to full reconcile when watcher state is untrusted; updates totalChanges.
Stop() and watcher health handling
packages/local-mount/src/auto-sync.ts
stop() optionally waits a short settle window when watchers are healthy, then drains pending paths or runs full reconcile based on watchersHealthy(), and ensures cleanup of timers/subscriptions.
Launch readiness gating and final syncBack
packages/local-mount/src/launch.ts
Track autoSyncReadyBeforeWrites from autoSync.ready(); finalize() uses autoSync.getDirtyPaths() and autoSync.watchersHealthy() to optionally pass scoped paths into handle.syncBack({ paths }), else falls back to full syncBack.
Mount.syncBack path filtering implementation
packages/local-mount/src/mount.ts
syncBack(opts?: { paths? }) resolves and deduplicates provided relative paths via syncBackPathsToFiles / resolveSyncBackSource; when paths is provided it skips full directory listing and uses setTimeout-based yields.
Tests for dirty-path tracking & path filtering
packages/local-mount/src/auto-sync.test.ts, packages/local-mount/src/mount.test.ts, packages/local-mount/src/launch.test.ts
Add tests exercising dirty-path tracking, flushPending(), syncBack({ paths }) filtering, and launch readiness gating/fallback; update mocks to include new AutoSyncHandle methods.
Documentation and changelog
packages/local-mount/README.md, packages/local-mount/CHANGELOG.md
Document syncBack({ paths }), flushPending, getDirtyPaths, watchersHealthy, and readiness-gated launch shutdown behavior; add CHANGELOG entries.
Trajectory and index metadata
.trajectories/compacted/*.json, .trajectories/compacted/*.md, .trajectories/index.json
Add compacted trajectory JSON/MD records capturing the decision about gating launch fast-path on watcher readiness and using dirty-path final sync; update index timestamp.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

"🐰 I sniffed the watcher trails with care,
Queued the dirt and chose what's rare,
I flushed the few, skipped sweeping wide—
Hop, sync, small steps: tidy pride. 🥕"

🚥 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 and concisely summarizes the main change: enabling syncBack to use a dirty-path fast path instead of full-tree walk when auto-sync watcher state is healthy.
Description check ✅ Passed The description is well-structured and directly related to the changeset, explaining the three key improvements and linking to the closed issue.
Linked Issues check ✅ Passed The PR fully addresses issue #134 by adding dirty-path APIs (getDirtyPaths, flushPending, watchersHealthy), enabling syncBack to accept optional paths argument, and implementing safety checks to fall back to full walk when watcher state is degraded or dirty set is empty.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #134's objectives: AutoSyncHandle enhancements, syncBack path filtering, watcher health tracking, and launch-phase readiness gating. Trajectory/compaction files are meta-artifacts unrelated to code functionality.

✏️ 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-134-dirty-syncback

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83f6dd5be2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +95 to +97
const finalSynced = await handle.syncBack({
signal: opts.shutdownSignal,
...(finalSyncBackPaths ? { paths: finalSyncBackPaths } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve full sync-back fallback when dirty path set is empty

When watchersHealthy() is true, finalize() always passes paths to syncBack, including an empty array, which skips the full tree walk entirely. After this commit, autoSync.stop() also skips runReconcile() on the healthy path (it only flushes queued watcher paths), and launchOnMount() never waits for autoSync.ready() before spawning the child. In short-lived runs, mount edits that occur before any watcher path is queued can now be dropped at shutdown because neither a full reconcile nor a full syncBack() runs.

Useful? React with 👍 / 👎.

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

🧹 Nitpick comments (1)
packages/local-mount/src/mount.ts (1)

204-204: 💤 Low value

Minor inconsistency: setTimeout vs setImmediate for yielding.

This line uses setTimeout(resolve, 0) while yieldToEventLoop() at line 351 uses setImmediate. Both yield control, but setImmediate executes after I/O callbacks and before timers, while setTimeout(..., 0) schedules a timer callback. The functional difference is negligible here, but using setImmediate consistently (or extracting to the existing helper) would be cleaner.

♻️ Suggested consistency fix
        if (signal && syncedForFile > 0 && !signal.aborted) {
-          await new Promise<void>((resolve) => setTimeout(resolve, 0));
+          await new Promise<void>((resolve) => setImmediate(resolve));
        }
🤖 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/mount.ts` at line 204, Replace the one-off await new
Promise<void>((resolve) => setTimeout(resolve, 0)) with the existing event-loop
yielding utility to keep behavior consistent; locate the statement in mount.ts
(the await new Promise... call) and either call the helper function
yieldToEventLoop() used elsewhere or use setImmediate via the same pattern as
yieldToEventLoop(), ensuring the same scheduling semantics as the usage at line
351.
🤖 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.

Nitpick comments:
In `@packages/local-mount/src/mount.ts`:
- Line 204: Replace the one-off await new Promise<void>((resolve) =>
setTimeout(resolve, 0)) with the existing event-loop yielding utility to keep
behavior consistent; locate the statement in mount.ts (the await new Promise...
call) and either call the helper function yieldToEventLoop() used elsewhere or
use setImmediate via the same pattern as yieldToEventLoop(), ensuring the same
scheduling semantics as the usage at line 351.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d2ef517f-7ba4-497d-bd25-b4d09c8b63bc

📥 Commits

Reviewing files that changed from the base of the PR and between 4d2ea77 and 83f6dd5.

📒 Files selected for processing (11)
  • .trajectories/compacted/compact_5kbxmj590rd2_2026-05-11.json
  • .trajectories/compacted/compact_5kbxmj590rd2_2026-05-11.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/auto-sync.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

@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 bugs or issues to report.

Open in Devin Review

@willwashburn willwashburn force-pushed the fix-134-dirty-syncback branch from 83f6dd5 to 4a50f74 Compare May 11, 2026 20:16
Address @chatgpt-codex-connector P1 on #138: even when watchers stay
healthy, the dirty-path set can be empty at shutdown — short-lived runs
where the watcher hadn't enqueued anything before stop(), or edits the
watcher missed via filesystem races. The previous code took the
fast path unconditionally and passed `paths: []` to handle.syncBack(),
which iterates zero files and skips the walk entirely. Combined with
autoSync.stop() no longer running a full reconcile on the healthy path
(it only flushes queued watcher paths), those mount edits would never
propagate to projectDir.

Only take the fast path when there's at least one dirty path. An empty
dirty set falls through to the full walk as a safety net. The cost is
trivial on no-change runs (the walker exits with synced=0 quickly) and
restores the dropped-edits invariant.

Add a regression test pinning the contract: watchersHealthy=true +
getDirtyPaths empty → syncBack called without `paths`, i.e. full walk.

Defer the mount.ts setTimeout→setImmediate nitpick from coderabbitai:
the existing "syncBack returns a partial count when aborted mid-walk"
test schedules controller.abort() via setTimeout, and matching the same
queue is what makes the abort observable between file syncs. Switching
to setImmediate races the abort and the test fails. Added an inline
comment explaining the intentional choice.

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

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

🧹 Nitpick comments (1)
packages/local-mount/src/launch.test.ts (1)

201-239: ⚡ Quick win

Add explicit stop() assertion on readiness-failure path.

This test checks fallback behavior, but it doesn’t verify watcher teardown after ready() throws. Adding that assertion would harden leak prevention on the error path.

Proposed test hardening
   it('falls back to full syncBack when auto-sync readiness fails', async () => {
     mkdirSync(mountDir, { recursive: true });

     let syncBackPaths: string[] | undefined;
+    let stopCalled = false;

     const createSpy = vi.spyOn(mountModule, 'createMount').mockResolvedValue({
       mountDir,
       startAutoSync: () => ({
-        stop: async () => {},
+        stop: async () => {
+          stopCalled = true;
+        },
         flushPending: async () => 0,
         reconcile: async () => 0,
         getDirtyPaths: () => ['tracked.txt'][Symbol.iterator](),
         watchersHealthy: () => true,
         totalChanges: () => 0,
@@
       expect(result.exitCode).toBe(0);
       expect(syncBackPaths).toBeUndefined();
+      expect(stopCalled).toBe(true);
     } finally {
       createSpy.mockRestore();
     }
   });
🤖 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.test.ts` around lines 201 - 239, The test
should assert that the watcher teardown is invoked when startAutoSync().ready()
throws: replace the inline stop: async () => {} with a spyable async function
(e.g., create a stopSpy = vi.fn(async () => {}) and return stop: stopSpy from
the mocked startAutoSync object), then after awaiting launchOnMount(...) add
expect(stopSpy).toHaveBeenCalled(); keep restore of createSpy as-is. Target
symbols: createMount, startAutoSync, stop, and launchOnMount.
🤖 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.

Nitpick comments:
In `@packages/local-mount/src/launch.test.ts`:
- Around line 201-239: The test should assert that the watcher teardown is
invoked when startAutoSync().ready() throws: replace the inline stop: async ()
=> {} with a spyable async function (e.g., create a stopSpy = vi.fn(async () =>
{}) and return stop: stopSpy from the mocked startAutoSync object), then after
awaiting launchOnMount(...) add expect(stopSpy).toHaveBeenCalled(); keep restore
of createSpy as-is. Target symbols: createMount, startAutoSync, stop, and
launchOnMount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 491a1bcd-00a8-41e3-99a7-efa5d2f3d262

📥 Commits

Reviewing files that changed from the base of the PR and between 4a50f74 and d0d8801.

📒 Files selected for processing (3)
  • packages/local-mount/src/launch.test.ts
  • packages/local-mount/src/launch.ts
  • packages/local-mount/src/mount.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/local-mount/src/mount.ts
  • packages/local-mount/src/launch.ts

@khaliqgant khaliqgant merged commit fe446d5 into main May 11, 2026
7 checks passed
@khaliqgant khaliqgant deleted the fix-134-dirty-syncback branch May 11, 2026 20:30
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.

local-mount: syncBack walks the full mount tree O(files); should be O(changes) via the auto-sync state

2 participants