Repository navigation
feat(sync-agent): move the agent's configuration into per-machine app settings #104
Description
Split out of #56, which scoped this in only if the app-settings read/write half turned out to be solid. It was not — three of its six actions had UI that could not work and three had none — so the migration correctly waited. PR #102 fixes that half, which makes this the next step rather than a speculative one.
Why per-machine settings are the right home
/help/app-settings frames per-machine settings as exactly this case: a value that is meaningful on one machine only. The Document Sync Agent's sync-folder path is the textbook example — it is a local filesystem path, and syncing it to another Mac would point that Mac at a folder that may not exist.
Today the agent keeps its configuration in UserDefaults, which means:
- it does not survive a clean reinstall
- a second Mac has to be configured from scratch with no indication that is expected
- the Devices pane can show you a machine and tell you nothing about what that machine is actually doing
What moves
The agent's state, roughly: enabled/disabled, the sync folder path (as a security-scoped bookmark, not a plain string — a sandboxed app cannot reopen a user-chosen folder from a path alone), last-sync timestamp, and whatever conflict policy it holds.
The parts that need care
Bookmarks are machine-local by definition. A security-scoped bookmark resolved on another Mac is meaningless. So the stored shape should be the bookmark plus a human-readable path for display, and the receiving machine should treat a foreign bookmark as "not configured here" rather than attempting to resolve it.
Migration must be one-way and idempotent. Read UserDefaults, write the per-machine document, keep reading UserDefaults as a fallback until a successful write is confirmed. A migration that runs on every launch and overwrites a newer server value with a stale local one is worse than no migration.
The writes are compare-and-set. PR #102 established that the app-settings family requires baseVersion and answers 409 with the current document on a stale write. The agent runs in the background, so it is the most likely thing in the app to lose that race — it needs a re-base-and-retry, not a failure toast.
Do not put it in shared settings. Shared is seeded to a new machine on first sign-in; a sync-folder path arriving pre-filled from a different Mac is the specific failure this whole feature exists to avoid.
Acceptance
- The agent reads its configuration from per-machine app settings, falling back to
UserDefaultsonly until the first successful write. - A folder chosen on one Mac does not appear as configured on another.
- A
409on the agent's own write re-bases and retries rather than surfacing. - The Devices pane can show whether a given machine has the agent configured.
- BDD quartet, including the boundary where
UserDefaultsis empty (a fresh install) and the one where both sources have a value.
Activity
Adron commentedon Sep 16, 2026
Implemented in PR #107. Work continues on the PR.
Stacked on #102 (which is where the working app-settings read/write half lives) and carries #99. Review order: #99 → #102 → #107.
The blocker nobody had named
Two processes, two bundle ids, two
UserDefaultsdomains — so the agent had no way to learn which device document is its own. The device id now rides the shared Keychain group that already carries the bearer token, so there is no new entitlement and no re-provisioning. The app mints and publishes it; the agent reads only.What honours each constraint in this issue
.absentvs.unavailablebeing distinct. Collapsing them is precisely the "worse than no migration" failure this issue names; the flag flips only after a confirmed write.documentSync., because it replaces wholesale and the document is shared with the main app.Two things deliberately not moved, with reasons in the code
The ledger's
lastSyncAtis the delta cursor and only means anything beside the per-document entries in the same file; what ships instead is a reported timestamp the pane reads and the engine never consumes. And there is no conflict policy to move —ConflictResolveris a fixed remote-wins table with no user-facing setting.com.interlinedlist.shared. Unsigned builds geterrSecMissingEntitlement, handled as "nothing published" so the agent stays onUserDefaults— correct failure mode, tested, but the signed happy path across the process boundary has not been observed.Both belong in the #63 on-device validation pass.
fix(sync-agent): publish the device id off the launch path