Repository navigation
fix(core): rebase directory watcher events across symlinked roots - #54093
kitlangton wants to merge 1 commit into
Conversation
|
Nice. I noticed today that hot reloading config does not work when the config dir is a symlink. I was just about to bring a similar PR when I learned you already fixed it here. :) |
|
Independent confirmation of the rebase half, from a fix that tried to get away with resolution alone. I opened #56294 to fix #54933 by resolving each watched directory inside That is Two things from that attempt that may be useful for this PR's tests: The refused subscription is a concrete failure, not just missing events. On a real inotify backend, subscribing to the link is rejected outright: So a test can distinguish "subscription failed" from "event dropped by a filter" — they fail the same way downstream (no reload) but for different reasons, and #54933's log shows the first while the filter bug produces the second. A watch-level assertion is not enough. My test subscribed to the planned path, wrote through the link, and asserted the emitted The rebased spelling is the part my attempt missed, so I assume your new test in |
48be7be to
ed6cbe5
Compare
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Why
When OpenCode watches a config or plugin directory reached through a symlink (such as macOS temp roots under
/var/folders→/private/var/foldersor/tmp→/private/tmp, symlinked project checkouts, or a symlinked~/.config/opencodedotfiles directory),@parcel/watcheron macOS (fs-events) emits canonicalrealpathevent paths whileConfigentries retain the subscribed directory spelling. Domain subscribers (ConfigPluginSource,ConfigCommandPlugin,ConfigAgentPlugin) filterconfig.changes()withFSUtil.contains(path.join(entry.path, ...), update.path), which returnsfalseacross the two spellings and silently drops live additions, edits, and removals (causing theplugin-registryDrive regression probe to time out waiting for a newly written.opencode/plugins/beta/index.tsto activate). In addition,@parcel/watcher/wrapper.jsresolves literalignoreentries (node_modules,.git) against the unresolved directory string beforeFSEventscanonicalizes the watched root, soignorePathsfails to match under symlinked directories on macOS, and@parcel/watcher'sinotifybackend does not traverse a symlinked root directory unless resolved upfront.What Changes
directory(e.g./var/folders/.../.opencode)@parcel/watcherreceives unresolveddirectory; macOSfs-eventsemits/private/var/folders/...paths that failFSUtil.contains(entry.path, update.path), and Linuxinotifyskips symlinked target rootsWatcherresolvesresolved = FSUtil.resolve(directory)for@parcel/watcherand rebases emitted event paths underresolvedback ontodirectoryignoreentries (node_modules,.git) under a symlinked directory@parcel/watcherresolvesignorePathsagainst the unresolved path, missing canonicalFSEventsevent paths@parcel/watcherreceivesresolved, soignorePathsand native events share the canonical prefix across all backendsConfigPluginSource,ConfigCommandPlugin, andConfigAgentPlugindropconfig.changes()eventsupdate.pathmatchesentry.path, triggering live reloads without a server restartflowchart LR Sub["Watcher.subscribe({ path: directory, type: \"directory\", ignore })"] --> Res["resolved = FSUtil.resolve(directory)"] Res --> Parcel["native.subscribe(resolved, callback, { ignore, backend })"] Parcel --> Ev["Native event: update.path under resolved"] Ev --> Rebase["Rebase update.path from resolved onto directory"] Rebase --> Match["FSUtil.contains(entry.path, update.path) === true"]Scope
Owns canonical target resolution and event path rebasing for recursive directory watches in
Watcher.nativeLayer. Single-file and immediate-entry watches (node:fs.watch) already construct event paths frominput.target.Verification
origin/v2on macOS FSEvents under default/var/foldersTMPDIR):plugin-registryDrive probe: Phase 1 (alphacold start) passes, Phase 2 (fs.writeFile(".opencode/plugins/beta/index.ts", ...)) times out after 20s withplugin-registry failed: Error: timed out: plugins alpha,beta without -.test/filesystem/watcher.test.tsandtest/config/command.test.tsfail (time out waiting for rebased watch events) on both macOS (fs-events) and Linux (inotify).inotify):plugin-registryDrive probe completes all three phases (cold start withalpha, runtime addition ofbeta, runtime removal ofbeta) with{"requests":3}on macOS (TMPDIR=/var/folders/...) and on Linux (both default/tmpand symlinkedTMPDIR).packages/corefull suite: 6606 pass, 33 skip, 0 fail across 261 files.bun run check: 36/36 packages pass typecheck and lint.