Skip to content

feat(sync-agent): move the agent's configuration into per-machine app settings #104

Description

@Adron

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 UserDefaults only until the first successful write.
  • A folder chosen on one Mac does not appear as configured on another.
  • A 409 on 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 UserDefaults is empty (a fresh install) and the one where both sources have a value.

Activity

  1. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    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 UserDefaults domains — 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

    • Bookmarks are stored with the device id that created them. A foreign or unstamped one is dropped on read and the Mac reports unconfigured — defence against the two routes that genuinely move a document across machines (copy-to-shared, main-workstation seeding).
    • The migration turns on .absent vs .unavailable being distinct. Collapsing them is precisely the "worse than no migration" failure this issue names; the flag flips only after a confirmed write.
    • 409 re-bases and retries, bounded at 3. The agent's own client keeps the typed 409 body, so the re-base costs zero extra requests.
    • Shared settings are untouched — the PUT preserves every key outside 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 lastSyncAt is 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 — ConflictResolver is a fixed remote-wins table with no user-facing setting.

    ⚠️ Two things unverifiable without hardware, both flagged in the PR

    1. No second Mac. The foreign-bookmark rule is proven by unit tests over the stored payload; the end-to-end "folder chosen on Mac A does not show configured on Mac B" walkthrough is not verified.
    2. No notarised build. The shared-Keychain channel needs both bundles signed with com.interlinedlist.shared. Unsigned builds get errSecMissingEntitlement, handled as "nothing published" so the agent stays on UserDefaults — 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions