ADR-043 — Sync v2: Machine Identity and Near-Real-Time Convergence
- Status: Accepted
- Spec: 010-sync (amended; slices also amend 007-memory and 009-channels)
- Amends: ADR-016 (keeps the git medium and the export→merge→import engine; replaces the interval-only auto-sync and the delete-by-absence import rule)
Context
ADR-016 shipped whole-vault sync over a user-owned git repo, aimed at machines that take turns. The actual usage is two machines in parallel, with different usernames / directory layouts, wanting convergence in tens of seconds. Auditing the shipped engine against that revealed:
- Auto-sync is interval-only (default 300 s); the "debounced push-on-change" ADR-016 promised was never built.
- Channel resources sync with
enabled: trueand the runtime starts every enabled channel — two machines fight over one Telegram bot, and pairing state (channel_peers) stays behind. - Import deletes by absence, so a resource that fails to import on one machine (e.g. a machine-local path) is later deleted everywhere.
- Absolute paths in configs — and memory's project IDs, which are hashes of the absolute git-root path — break across machines with different homes.
- Shared intent (MCP tool preferences, engine configs, skill-delivery intent, channel pairing) never syncs.
- There is no model concept of "a machine" to hang any of this on (
machine_idwas justplatform.node()in commit messages).
Decision
Keep the git transport and engine skeleton; add a thin machine-identity layer and fix the findings. The pieces (delivered as independent slices):
- Machine identity. A stable per-installation ULID + editable display name (DB singleton, never synced as data). The workspace gains a
machines/<id>.jsonregistry area where each machine writes only its own entry — visibility without conflicts. - Runtime affinity. Channel configs carry
runs_on: <machine_id>; only the bound machine starts the adapter; pairing identity syncs so rebinding needs no re-pairing.null(post-upgrade default) runs nowhere until the user picks a machine. - Path portability.
${HOME}normalization on export/import for config string values, plus per-machine JSON-Merge-Patch overrides undermachines/<id>/overrides/for genuinely divergent values. Memory project IDs derive from the normalizedoriginremote URL (path-hash fallback + one-time alias migration), so the same repo maps to the same memory store on every machine. - Tombstoned deletion. Deletes travel as explicit
tombstones/resources/<kind>/<name>.jsonfiles; absence alone never deletes. Failed imports are quarantined and preserved, not dropped. - Near-real-time engine. In-daemon change notifications + a
watchfileswatcher over the three file trees (agents can write memory through symlinks, bypassing the daemon) feed a debounced push; a lightweightgit ls-remotehead probe (default every 15 s) triggers pulls. The interval sweep remains as a fallback. Expected convergence ≈ 15–30 s.
Machine identity carries exactly three responsibilities — affinity, overrides, visibility — plus provenance labels on tombstones. It is not a per-record versioning scheme.
Alternatives considered
- Per-record version vectors / operation log — the textbook fix for finding 3; rejected as a distributed-systems tax on a single-user tool. Git 3-way merge over per-ULID files already handles the file-tree domains well.
- Hub / leader machine — one always-on machine runs everything and relays state; rejected: breaks symmetry (laptop closes → channels die) and doesn't address paths.
- Migration-only (backup/restore) — considered when re-scoping; rejected because the machines are used in parallel, and overwrite semantics would discard the other machine's changes.
- Real push transport (relay / P2P) — sub-second convergence but re-opens ADR-016's medium decision; tens-of-seconds is achievable inside the existing constitutional posture, so not now.
Consequences
- Sync workspace layout grows
machines/,tombstones/,state/areas; the manifest schema version bumps (to 2) with the tombstone slice, so all machines must upgrade Coffer before the first v2 sync. - New backend dependency
watchfiles;git ls-remotepolling becomes a steady, small network egress while auto-sync is on. - The channel runtime consults machine identity — the first kind-level behavior keyed to a machine.
- A rebind can briefly double-run a channel during propagation; accepted for a single-user tool.
- Chat history, audit logs, and runtime state remain machine-local by design.