Repository navigation
chore(sync): stop reading the retired mirror_path key - #409
Merged
Merged
Conversation
ResolveConfig fell back to mirror_path in the workspace store and then in global state before the detected cloud default. The destination is the store's target now, so the fallback goes, with LocalConfig.MirrorPath, UserGsyncState.MirrorPath and every writer of them: `dot sync target` writes the store alone, and under --home it writes that home's store. A store or state file that still sets the old key is not edited quietly. Both write choke points announce the key once and name `dot sync target`, and keepUnknownConfigKeys exempts it, or the next save would carry it back forever. Part of #394.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
A store with no `target` resolves the detected cloud default, so a machine that still names another folder in `mirror_path` would start pushing (create+update+delete) and pulling against a tree nobody chose, with only a save-time note to say so later. The transfer entries now compare the retired value with the destination this config resolved, following symlinks on both sides, and stop when they differ: the error names the stored folder and `dot sync target <that folder>`. A same-directory match (the peer Mac's ~/Dropbox is a symlink into ~/Library/CloudStorage) proceeds untouched. Resolution deliberately does not refuse: `dot sync target`, the fix the error names, resolves a config of its own and would otherwise fail too. Part of #394.
The refusal that came with removing the `mirror_path` fallback had one piece of evidence, the retired value itself, and every routine save rewrote the file holding it while `target` was still empty: `dot sync pause`, `resume`, `owner`, `setup` and `shared` all save the store, and `dot apply` or `dot reconfigure` save global state. After one of those the store resolved the detected default, the refusal had nothing left to read, and the next push mirrored, and deleted, in a folder nobody picked. A retired value is evidence now, not a setting. A save writes it back unchanged, and says nothing, while it could still move a destination; it drops it with the existing note only once the value names the folder resolution would use. A store save judges `target` first and then the detected default; a state save judges that home's default, so `--home` writes are decided against the target home. A layout built without a home carries, which is the safe side of a doubt the save cannot settle. `dot sync fetch` copies mirror to workspace, so a destination that moved unasked would refill the workspace from a tree nobody picked and overwrite what is there. It joins the transfer entries that refuse. Also fixes the spelling of the retired state key: detection read `modules.gsync.mirror_path`, and neither the struct tag nor any real file has that block (`gdrive_sync`), so the live machine's key was never detected and the passing test only proved the wrong spelling.
entelecheia
added a commit
that referenced
this pull request
Nov 9, 2026
This was referenced Nov 9, 2026
entelecheia
added a commit
that referenced
this pull request
Nov 9, 2026
* chore(release): add v2.81.0 release notes Covers #399, #400, #401, #402, #404, #405, #407 and #409. #406 (spec docs) and #408 (test isolation) are left out per the release-note rules. Claude-Session: https://claude.ai/code/session_01Vb48Vt5iGMfc7MJWPTCWCL * chore(release): v2.81.0 notes, apply review findings Name the resume reload fix (#411), the peer pair upgrade, the exact anchor scope, the dot sync target change and the dot admit wait. Claude-Session: https://claude.ai/code/session_01Vb48Vt5iGMfc7MJWPTCWCL
entelecheia
added a commit
that referenced
this pull request
Nov 10, 2026
* fix(sync): reload the scheduler unit on resume `dot sync resume` rewrote a unit installed before #394 to `--store`, but neither service manager reads a rewritten unit file until it is told to. launchd keeps running the command line it cached when `load` finds the job already loaded (which a failing job usually is), and systemd keeps the ExecStart it read until `daemon-reload`. So the migration alone healed nothing, and the `pause && resume` recovery hint that `dot sync status` prints for a spawn-failed job did not work on either platform. ResumeKind now unloads the plist before loading it on macOS, ignoring the unload error the way InstallKind does, and runs a user-scoped `daemon-reload` before starting the timer on Linux. The lifecycle test helper pins the two-step resume, so a resume that skips the reload fails the tests that record service-manager argv. `dot sync target` no longer says a local target is recorded in the global user state for future workspaces: since #409 the target is written only to this workspace's store, and a workspace without one uses the detected cloud folder. Part of #394. * test(sync): expect only the command that fails in a failed resume The lifecycle row that injects a failure asserted every resume command for the kind. On Linux that is wrong: a failed `daemon-reload` returns before the timer is started, so `start` never runs. macOS differs only because launchd's unload error is ignored, which puts the failure on the load. The expectation now stops at the failing command on the platforms whose resume returns there, which is what the two implementations do. Product code is unchanged; the row is the only thing that was wrong, and only the ubuntu unit job can execute it. * test(sync): count daemon-reload as a lifecycle action The installed-kind check treats a row that recorded no lifecycle action as a vacuous fixture. The Linux resume row under an injected failure records `--user daemon-reload` alone, which the counter did not recognise, so the comparison passed and the row still failed on the counter (ubuntu unit CI). The prefix is now counted, and a small table test pins the counter on both platforms so this Mac notices an unrecognised command next time instead of the ubuntu job. * style(sync): spell recognize the US way in a comment The repo's misspell linter runs with locale US, so the comment I added over TestSchedulerLifecycleActionCount failed `make lint` on one word. Comment text only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #394 (row 10).
What changes
ResolveConfigno longer falls back tomirror_path: the workspace store'stargetdecides the destination, else the detected cloud default. That was the last reader of the
key, so
LocalConfig.MirrorPath,UserGsyncState.MirrorPathand their writers are gone:dot sync targetwrites the workspace store alone. Under--homeit now writes thetarget home's store; before, only global state was home-aware, and the destination for
a new workspace came from that state.
dot sync configure --targetno longer mirrors the path into the store's old key.SeedFromGlobalseeds a new store with an emptytarget, so the detected cloud defaultapplies until someone sets one.
A file that still carries the key is not edited quietly. Both write choke points
(
saveStateAt,SaveLocalConfig) say once what happened to it and namedot sync target, then clear the flag so a second save in the same run is silent.keepUnknownConfigKeyshas to exempt the retired key, or every save would carry it backand the note would be a lie.
A destination that would move without being asked is refused
Removing the fallback alone would move a store that never set
target: its destinationbecomes the detected default even when the store, or the global state it was seeded from,
still names another folder in
mirror_path. Push and pull both propagate creates, updatesand deletes, so
dot sync pushon such a machine could mirror into a tree nobody chose anddelete there, with only a save-time note to say so later.
So the key is read in one place still, and only to refuse. When a store's
targetis emptyand its
mirror_path(else the global state's) is not, the transfer entries (Push,PullDirect,Intake,PullTracked,PlanPush,Fetch) compare the stored folder withthe destination this config resolved and stop before a transfer when they differ:
Fetchis included because it copies mirror to workspace: a destination that movedunasked would refill the workspace from a tree nobody picked and overwrite what is there.
peer Mac's
~/Dropboxis a symlink into~/Library/CloudStorage/Dropbox). A path thatdoes not resolve is compared textually, which leans towards refusing.
would use instead, and both
dot sync targetfixes.ResolveConfig, because the fix theerror names is
dot sync target, which resolves and writes a config of its own.Nothing routes through the key: a folder that matches proceeds with the default already
resolved, and a differing one never reaches a transfer.
The refusal is not one-shot
The refusal's only evidence is the retired value, and every routine save rewrites the file
that holds it, while
targetis still empty:dot sync pause/resume(the reload thischange tells users to run),
dot sync owner,dot sync setup,dot sync sharedall gothrough
SaveLocalConfig, anddot applyordot reconfigurerewrite global state. Asave that dropped the key there would hand the next push the detected default, which is
exactly the redirect above.
So a retired value is treated as evidence, not a setting. A save carries it through
unchanged, and says nothing, while it could still move a destination:
targetis empty and the value is not the same directory as the detected default.Once the value names the folder resolution would use it decides nothing, so that save drops
it and prints the note. Both decisions use the symlink-aware comparison, and a layout built
without a home carries the value (the safe side of a doubt the save cannot settle).
--homewrites judge against the target home's default, never the invoking user's.Evidence: neither Mac depends on it
mirror_pathstill exists on both machines, and on neither does any store's destinationchange without it: every live store sets
target, which is read first.This Mac:
~/.config/dotfiles/config.yamlline 35:mirror_path: ~/Dropbox/workundermodules.gdrive_sync, and notargetkey. That is the shape the carry rule protects.<workspace>/.dotfiles/sync/config.yaml:target: local:~/Dropbox/work/plusmirror_path: ~/Dropbox/work/.<workspace>/.dotfiles/peer/config.yaml:target: ssh:...only.The peer Mac, over ssh:
~/Dropbox/secrets, sothe default returns
~/Dropbox/work. The peer has~/Library/CloudStorage/Dropbox/secrets(its~/Dropboxis a symlink to that root), sothe default returns
~/Library/CloudStorage/Dropbox/work, the same physical tree.Verification
dot admit -- env GOMAXPROCS=2 go test -p 2 -parallel 2 ./internal/config ./internal/syncer ./internal/cli ./internal/fileutilpasses.dot admit -- make lint: 0 issues.targetandmirror_pathloads, saves without thekey and prints the note once; a stamped state file whose value names the default does the
same; a store with only
mirror_pathresolves the detected default.folder and both fixes; the same folder through a symlinked cloud root does not refuse; a
store whose global state names a folder refuses through that source;
Fetchrefuses andwrites nothing; a store that sets
targetis unaffected.mirror_pathsavesthrough
dot sync pausewith the key still in the file and no note, andPlanPushon thenext resolve still refuses; the same store with the default value in
mirror_pathdropsthe key and prints the note; a state file with another folder carries through a save.
internal/fileutil: the same folder through asymlink, a trailing separator, and two spellings of one missing path compare equal; a
missing side and two different missing paths do not.
mechanism and watching it fail: with the carry disabled the save prints a drop note and
PlanPushstops refusing; with theFetchentry disabled the fetch runs.Release note:
mirror_pathis no longer read for the sync destination;dot sync targetsets it.