Read first: Asynchronous Calls (everything here is reactive, IoPool-bounded) and Access Context Propagation (worker writes run as system). The GitHub counterpart of this feature is the GitSync engine; instance sync reuses its config-satellite and settings-tab shape.

What it does

From any Space's Settings → Instance Sync tab, a Space admin adds a syncing party: the URL of another MeshWeaver instance plus an ApiToken issued there. The Space is replicated to the remote instance, and from then on changes keep syncing as they happen — like syncing a SharePoint library. If the other instance is down, changes accumulate in a durable manifest and are applied when it can be reached again. The tab lists every syncing party with its live status, and each party can be paused (Active off), poked (Sync now) or removed (cancel). The same sources also appear on the platform-admin Partitions page through the IPartitionSyncSourceProvider seam (kind: Remote Instance).

The registry: `

Each syncing party is one MeshNode at {space}/_Sync/{sourceId} with content InstanceSyncConfig (project MeshWeaver.InstanceSync):

Field Meaning
RemoteUrl, RemoteToken The remote portal and the ApiToken issued by that remote
RemoteSpace Space id on the remote (blank = same id)
Direction Bidirectional (default) / PushOnly / PullOnly
Active Uncheck to pause — changes still accumulate, nothing transfers
SyncRequestedAt Control-plane trigger stamped by "Sync now" (the Requested-field pattern)
Status, LastSyncedAt, LastError Worker-written, read-only in the GUI
PendingChanges The durable change manifest (see below)

_Sync is a _-prefixed satellite segment, so every content filter (the GitHub export filter and the instance-sync push filter itself) excludes it: the registry — including the token — never replicates anywhere, and the worker's own status/manifest writes never re-enter the sync loop. The GUI edits the config through the standard node-content editor bound directly to the node stream; there is no hand-rolled form or replica.

Architecture: the registering instance is the sync client

The remote instance needs nothing but its existing MCP surface (get / create / update / delete / search, reached through McpRemoteMeshClient with Authorization: Bearer — the same client the mirror feature uses). All sync logic runs on the instance that holds the registration, exactly like a SharePoint/OneDrive sync client:

IMeshChangeFeed ──► InstanceSyncCoordinator (IHostedService, one per process)
                       │  config-node events ⇒ start/stop/poke workers
                       │  content events     ⇒ offer to each worker
                       ▼
                InstanceSyncWorker (one per registration)
                  PUSH: coalesce into PendingChanges manifest (stream.Update, durable)
                        └─ drain pipeline (Subject → Sample → Concat, serialized)
                            └─ per node: Get remote → equal? skip : Create/Update/Delete
                  PULL: periodic sweep — search hits carry version + lastModified,
                        Get only changed nodes, newest-writer-wins, apply as system

Loop prevention (the echo problem)

Applying a pulled change locally fires the local change feed, which would re-enter the manifest and push the change straight back — an infinite ping-pong. Two independent layers stop it:

  1. Consume-once suppression — the pull-apply registers the local path in an instance-scoped registry before writing; the very next feed event for that path is swallowed exactly once.
  2. Value-equality convergence guard — every push and pull first compares content (name/type/state + canonical JSON) and drops value-equal writes. Any echo that slips past layer 1 terminates after one hop instead of oscillating.

Conflict resolution (newest-writer-wins, symmetric → the two instances converge)

When the same node is edited on both instances within a sweep interval, the resolution is newest-writer-wins by LastModified, applied symmetrically on BOTH the pull and the push side, so the two instances always converge on the newer write:

Net effect: the node with the greatest LastModified wins on both sides. Resolution is at whole-node granularity, not a field-level merge — two sides editing different fields of the same node still resolve to the newer whole node (the older side's field edit is superseded).

Known limitations (v1)

Permissions (a Space-editor capability)

Configuring or running instance sync is an editor-level operation, gated on Permission.Update on the Space — the right that comes with the Editor role (and Admin/Owner); Viewer/Commenter cannot. The "Synchronizations" node-menu item and the sync management area self-gate on it (InstanceSyncMenuProvider), so the whole feature is invisible to users who can only read the Space. (This is distinct from Permission.Sync, which governs the static-repo import overwrite of read-only partitions and is deliberately NOT part of Editor/All.)

Testing

test/MeshWeaver.InstanceSync.Test runs the full loop against an in-memory IRemoteMeshClient fake (the interface's sanctioned test seam) with tight intervals: initial replication, incremental push, offline accumulation → reconnect drain, restart resume, pull, echo suppression, and direction gating. InstanceSyncConflictTest covers the conflict/round-trip/offline guarantees end-to-end: local↔remote round-trip convergence, concurrent same-node edits (newer wins on both sides, remote-newer and local-newer), the offline "long flight" (accumulate while unreachable → drain → converge on reconnect), and whole-node resolution of different-field concurrent edits.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.