diff --git a/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.json b/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.json new file mode 100644 index 0000000..7b5526b --- /dev/null +++ b/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.json @@ -0,0 +1,53 @@ +{ + "id": "traj_d8emfg3tc8yi", + "version": 1, + "task": { + "title": "Reduce relayfile mount polling noise" + }, + "status": "completed", + "startedAt": "2026-04-17T13:28:00.972Z", + "completedAt": "2026-04-17T13:28:35.109Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-04-17T13:28:04.956Z" + } + ], + "chapters": [ + { + "id": "chap_disu8q4jucfi", + "title": "Work", + "agentName": "default", + "startedAt": "2026-04-17T13:28:04.956Z", + "endedAt": "2026-04-17T13:28:35.109Z", + "events": [ + { + "ts": 1776432484957, + "type": "decision", + "content": "Use file watching for CLI mount local writes: Use file watching for CLI mount local writes", + "raw": { + "question": "Use file watching for CLI mount local writes", + "chosen": "Use file watching for CLI mount local writes", + "alternatives": [], + "reasoning": "The CLI mount entrypoint was polling and scanning the local mirror every two seconds even though mountsync already has an fsnotify watcher. Wiring the watcher lets websocket mounts skip no-op cycles while still pushing local edits immediately." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Reduced mount-loop noise and local scan overhead by wiring fsnotify into the CLI mount command and skipping websocket no-op cycles in both mount entrypoints.", + "approach": "Standard approach", + "confidence": 0.92 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile", + "tags": [], + "_trace": { + "startRef": "df342b6d7c423b09a088577b6e4142a550d82062", + "endRef": "df342b6d7c423b09a088577b6e4142a550d82062" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.md b/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.md new file mode 100644 index 0000000..b8db512 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.md @@ -0,0 +1,31 @@ +# Trajectory: Reduce relayfile mount polling noise + +> **Status:** ✅ Completed +> **Confidence:** 92% +> **Started:** April 17, 2026 at 03:28 PM +> **Completed:** April 17, 2026 at 03:28 PM + +--- + +## Summary + +Reduced mount-loop noise and local scan overhead by wiring fsnotify into the CLI mount command and skipping websocket no-op cycles in both mount entrypoints. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Use file watching for CLI mount local writes +- **Chose:** Use file watching for CLI mount local writes +- **Reasoning:** The CLI mount entrypoint was polling and scanning the local mirror every two seconds even though mountsync already has an fsnotify watcher. Wiring the watcher lets websocket mounts skip no-op cycles while still pushing local edits immediately. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Use file watching for CLI mount local writes: Use file watching for CLI mount local writes diff --git a/.trajectories/index.json b/.trajectories/index.json index 01305e4..0fc622e 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-04-17T13:12:15.026Z", + "lastUpdated": "2026-04-17T13:28:35.212Z", "trajectories": { "traj_lcvvpzmuroqb": { "title": "file-observer-dashboard-workflow", @@ -57,6 +57,13 @@ "startedAt": "2026-04-17T13:06:18.420Z", "completedAt": "2026-04-17T13:12:14.864Z", "path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile/.trajectories/completed/2026-04/traj_nv8vh0eav3v0.json" + }, + "traj_d8emfg3tc8yi": { + "title": "Reduce relayfile mount polling noise", + "status": "completed", + "startedAt": "2026-04-17T13:28:00.972Z", + "completedAt": "2026-04-17T13:28:35.109Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile/.trajectories/completed/2026-04/traj_d8emfg3tc8yi.json" } } } \ No newline at end of file diff --git a/cmd/relayfile-cli/main.go b/cmd/relayfile-cli/main.go index 9da7bc4..d9e8b82 100644 --- a/cmd/relayfile-cli/main.go +++ b/cmd/relayfile-cli/main.go @@ -28,6 +28,7 @@ import ( "unicode/utf8" "github.com/agentworkforce/relayfile/internal/mountsync" + "github.com/fsnotify/fsnotify" ) const ( @@ -525,7 +526,7 @@ func runMount(args []string) error { return err } - return runMountLoop(rootCtx, syncer, *timeout, *interval, *intervalJitter, *websocketEnabled, *once) + return runMountLoop(rootCtx, syncer, absLocalDir, *timeout, *interval, *intervalJitter, *websocketEnabled, *once) } func runTree(args []string, stdout io.Writer) error { @@ -1598,7 +1599,7 @@ func jitteredIntervalWithSample(base time.Duration, jitterRatio, sample float64) return delay } -func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, timeout, interval time.Duration, intervalJitter float64, websocketEnabled, once bool) error { +func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir string, timeout, interval time.Duration, intervalJitter float64, websocketEnabled, once bool) error { runCycle := func(reconcile bool) error { ctx, cancel := context.WithTimeout(rootCtx, timeout) defer cancel() @@ -1621,6 +1622,22 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, timeout, in return initialErr } + watcher, err := mountsync.NewFileWatcher(localDir, func(relativePath string, op fsnotify.Op) { + ctx, cancel := context.WithTimeout(rootCtx, timeout) + defer cancel() + if err := syncer.HandleLocalChange(ctx, relativePath, op); err != nil { + log.Printf("mount local change failed: %v", err) + } + }) + if err != nil { + return fmt.Errorf("create file watcher: %w", err) + } + if err := watcher.Start(rootCtx); err != nil { + _ = watcher.Close() + return fmt.Errorf("start file watcher: %w", err) + } + defer watcher.Close() + timer := time.NewTimer(jitteredIntervalWithSample(interval, intervalJitter, mathrand.Float64())) defer timer.Stop() cycle := 0 @@ -1632,7 +1649,9 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, timeout, in case <-timer.C: cycle++ reconcile := !websocketEnabled || cycle%websocketReconcileEvery == 0 - _ = runCycle(reconcile) + if reconcile { + _ = runCycle(true) + } timer.Reset(jitteredIntervalWithSample(interval, intervalJitter, mathrand.Float64())) } } diff --git a/cmd/relayfile-mount/main.go b/cmd/relayfile-mount/main.go index baf9733..b74a2cd 100644 --- a/cmd/relayfile-mount/main.go +++ b/cmd/relayfile-mount/main.go @@ -22,8 +22,9 @@ import ( ) const ( - mountModePoll = "poll" - mountModeFuse = "fuse" + mountModePoll = "poll" + mountModeFuse = "fuse" + websocketReconcileEvery = 10 ) var errFuseModeUnavailable = errors.New("fuse mode is not available in this build") @@ -201,13 +202,18 @@ func runPollingMount(rootCtx context.Context, cfg mountConfig) error { rng := rand.New(rand.NewSource(time.Now().UnixNano())) timer := time.NewTimer(jitteredIntervalWithSample(cfg.interval, cfg.intervalJitter, rng.Float64())) defer timer.Stop() + cycle := 0 for { select { case <-rootCtx.Done(): log.Printf("mount sync stopping: %v", rootCtx.Err()) return nil case <-timer.C: - run(true) + cycle++ + reconcile := !cfg.websocketEnabled || cycle%websocketReconcileEvery == 0 + if reconcile { + run(true) + } timer.Reset(jitteredIntervalWithSample(cfg.interval, cfg.intervalJitter, rng.Float64())) } }