コンテンツにスキップ

Multi-Device Sync and Alignment

このコンテンツはまだ日本語訳がありません。

Full spec: 00-product-spec.md.

Scenario Challenge
Work Mac + personal Mac Knowledge must be partially shared while observation data stays isolated; the personal machine is occasionally used for work
Replacing a machine Knowledge must travel with the user, the PG index must be rebuilt, observation starts from zero
Heavy multi-device users (e.g. finance) 3-10 devices, strict partitioning, auditing, controlled conflict resolution

Core tension: YZOS is local-first, but a user’s working memory naturally spans devices.

Data is split into four layers, each with a different sync policy:

Layer Content Synced across devices Reason
L1 knowledge source of truth OKF (memory / knowledge / entities) Synced Small, git-friendly, the core of the supply layer
L2 manual overrides entity_overrides, voiceprints, non-secret config Synced User corrections should apply globally
L3 index PostgreSQL Not synced, rebuilt per device Can be rebuilt from L1, avoids syncing PG binaries
L4 raw observation Screenshots, voice, AI session snapshots Not synced by default Large, privacy-sensitive, and tied to device context

The upper-layer AI queries L1+L3 through MCP. Once L1 is aligned, each device rebuilds L3 with its own scheduled reconcile.

Every OKF file is an independent Markdown file with frontmatter, which naturally supports:

  • git push/pull
  • Syncthing / iCloud folder sync
  • packaging with yzos export --bundle for migration

The PG index on any given device is only a local copy of the same set of OKF files.

Entries that crystallize into the knowledge base must record their source device so conflicts can be resolved when merging:

# entity frontmatter extension
device_id: macbook-pro-work
device_label: Work MacBook
context: work # work | personal | travel
first_seen_device: macbook-pro-work
devices_seen: [macbook-pro-work, macbook-air-life]

4. The user picks the sync backend, YZOS does not own a cloud

Section titled “4. The user picks the sync backend, YZOS does not own a cloud”

YZOS does not provide or operate a sync cloud service. Users bring their own infrastructure; YZOS is only responsible for:

  1. Defining the sync scope (which OKF directories participate in sync)
  2. Defining the alignment flow (pull -> merge -> reconcile -> push)
  3. Invoking a standard tool or watching an already-synced directory according to configuration

Two modes:

Mode Description Typical backends
Passive The user already syncs a folder with iCloud / Syncthing / etc.; YZOS only watches that directory for changes and reconciles iCloud Drive, Syncthing, Dropbox
Active The YZOS daemon runs pull/push on a schedule rsync+SSH, WebDAV, S3, Git
flowchart LR
    ORCH["YZOS Sync Orchestrator<br/>scope filter, merge, reconcile"]
    ORCH -->|passive watch| ICLOUD["iCloud folder"]
    ORCH -->|rsync over SSH| NAS["self-hosted NAS / SSH"]
    ORCH -->|webdav| DAV["Nextcloud / Office 365 / WebDAV"]
    ORCH -->|s3 object| S3["MinIO / AWS S3"]
    ORCH -->|git repo| GIT["GitHub / private Git"]
    ORCH -->|existing P2P| ST["user's Syncthing setup"]

All backends share the same top-level sync.toml configuration (scope, conflict, interval); only the [sync.backend.*] section differs.

Passive - folder watch (iCloud / an existing cloud sync)

Section titled “Passive - folder watch (iCloud / an existing cloud sync)”

The user places ~/.yzos/contexts/shared/ (or the whole sync bundle) inside a directory already synced by the OS or a third party:

[sync]
enabled = true
mode = "passive"
watch_dir = "~/Library/Mobile Documents/com~apple~CloudDocs/yzos-sync"
# or a directory already synced by Syncthing, e.g. ~/Sync/yzos/
[sync.scopes]
include = ["shared/**", "work/entities/**"]
exclude = ["personal/**", "activity/**"]

YZOS behavior:

  • FSEvents watches OKF file changes inside watch_dir
  • Once the external sync finishes, triggers reconcile and an incremental index rebuild
  • Makes no network requests of its own, relying entirely on the user’s iCloud / Syncthing / Dropbox

Best fit: two Macs, zero configuration, users who already use iCloud.

The user configures their own SSH target (home NAS, cloud host, office jump box):

[sync]
enabled = true
mode = "active"
backend = "rsync"
interval_minutes = 15
[sync.backend.rsync]
remote = "user@nas.local:/volume1/yzos-sync"
# or remote = "user@203.0.113.5:/home/user/yzos-sync"
ssh_config = "~/.ssh/config" # optional, use a Host alias
ssh_identity = "~/.ssh/id_ed25519_yzos"
rsync_args = ["-avz", "--delete-delay", "--exclude", ".sync_state"]
bandwidth_limit = "5m" # optional
[sync.scopes]
push = ["shared/**", "work/**"]
pull = ["shared/**", "work/**"]

YZOS executes:

Terminal window
# pull
rsync -avz user@nas:/volume1/yzos-sync/shared/ ~/.yzos/contexts/shared/
# push (only files inside scope with mtime newer than sync_state)
rsync -avz ~/.yzos/contexts/shared/ user@nas:/volume1/yzos-sync/shared/
  • Keys stay in the user’s ~/.ssh/; YZOS never stores passwords (or stores the passphrase in Keychain)
  • Supports ProxyJump (e.g. office -> home NAS)
  • The remote maintains .sync_state/manifest.json (file path + hash + device_id + mtime)

Self-hosted or third-party WebDAV (Nextcloud, Nutstore, Office 365, etc.):

[sync]
mode = "active"
backend = "webdav"
interval_minutes = 30
[sync.backend.webdav]
url = "https://cloud.example.com/remote.php/dav/files/alex/yzos-sync"
# credentials live in Keychain, never written to sync.toml in plaintext
auth = "keychain:yzos-webdav" # or basic / digest
tls_verify = true
timeout_secs = 120
[sync.scopes]
push = ["shared/**"]
pull = ["shared/**", "work/knowledge/**"]

Implementation: Rust reqwest plus WebDAV PROPFIND/GET/PUT/MKCOL, or a dav-server client library.

  • Incremental sync: compares remote ETag / Last-Modified against the local sync_state
  • Large files: chunked upload where the server supports it
  • Does not depend on rclone, but a user can still set backend = "rclone" to delegate to the external tool

AWS S3, MinIO, Cloudflare R2, or S3-compatible modes of other providers:

[sync]
mode = "active"
backend = "s3"
interval_minutes = 60
[sync.backend.s3]
endpoint = "https://s3.amazonaws.com" # MinIO: https://minio.home:9000
region = "ap-southeast-1"
bucket = "yzos-sync"
prefix = "devices/" # object key prefix
# credentials: Keychain stores AWS_ACCESS_KEY_ID / SECRET, or an IAM role (future)
[sync.scopes]
push = ["shared/**", "work/entities/**"]
pull = ["shared/**"]

Object key layout:

s3://yzos-sync/
-> manifest.json (global file manifest: path, hash, device_id, mtime)
-> shared/knowledge/projects/example-project.md
-> shared/entities/person/...
-> devices/macbook-pro-work/ (optional per-device staging area, merged into shared after review)
-> .staging/

Flow:

  1. pull: ListObjects -> compare against manifest -> download files whose hash differs
  2. merge: apply the local conflict strategy
  3. push: upload changed files -> update the manifest (optimistic locking via CAS)

Best fit: multi-machine hubs, finance users running their own MinIO, environments without a fixed IP.

[sync]
mode = "active"
backend = "git"
interval_minutes = 60
[sync.backend.git]
repo = "git@github.com:alex/yzos-knowledge.git"
branch = "main"
# only OKF text is synced; .gitignore excludes activity/ voice/ pg/
[sync.scopes]
paths = ["contexts/shared", "contexts/work/knowledge", "contexts/work/entities"]

Flow: git pull --rebase -> reconcile -> git add for changes inside scope -> git commit (message tagged with device_id) -> git push

  • OKF Markdown files are naturally suited to Git
  • Conflicts: a file-level .md merge conflict is flagged needs_review
  • Works with private GitHub, Gitea, or self-hosted GitLab

The user configures their own Syncthing sync folder; YZOS watches it in passive mode:

[sync]
mode = "passive"
watch_dir = "~/Sync/yzos-shared"

YZOS does not integrate with the Syncthing API (to avoid a hard dependency); it simply reuses P2P capability the user already has configured.

Advanced users can set backend = "rclone"; YZOS only generates an rclone config and invokes it:

[sync.backend.rclone]
remote = "nextcloud:yzos-sync" # a remote name already configured in the user's rclone.conf

A single rclone.conf can cover WebDAV, S3, Google Drive, and dozens of other backends; YZOS does not reimplement them.


Credential type Storage location Forbidden
SSH private key User’s ~/.ssh/; YZOS only references the path Writing it into sync.toml
WebDAV password macOS Keychain yzos-webdav Plaintext
S3 access key Keychain yzos-s3 Plaintext
Git System ssh-agent / credential helper Embedding in the repo

Sync traffic is not encrypted by default (it relies on transport-layer TLS / SSH). Users who need end-to-end encryption can wrap the scope in a gpg layer or use Syncthing’s device encryption on their own; YZOS does not mandate it.


Scenario 1: Work Mac + Personal Mac (occasional crossover)

Section titled “Scenario 1: Work Mac + Personal Mac (occasional crossover)”
Section titled “Recommended: dual context with partial sharing”
flowchart TD
    subgraph WORK["Work MacBook - context work"]
        WA["work apps"] --> WC["crystallize into work/"]
    end
    subgraph PERSONAL["Personal MacBook Air - context personal"]
        PA["personal apps"] --> PC["crystallize into personal/"]
    end
    WC --> SHARED
    PC --> SHARED
    subgraph SHARED["shared/ - shared knowledge layer"]
        PROJ["projects/"]
        MEM["common memory"]
    end

Directory layout:

~/.yzos/
-> contexts/
-> work/ (primary writes from the work machine)
-> knowledge/
-> memory/
-> entities/
-> personal/ (primary writes from the personal machine)
-> ...
-> shared/ (synced between both machines)
-> knowledge/projects/ (e.g. example-project, may be touched by either machine)
-> memory/ (cross-context memory)
-> entities/ (confirmed people / projects / tasks)
-> device.json (local device identity)
-> sync.toml (sync configuration)

Personal machine occasionally used for work

Section titled “Personal machine occasionally used for work”

When the personal machine detects “work mode” (manual switch, connecting to the company VPN, opening a work repo):

  1. context temporarily switches to work (or work@personal-device)
  2. New crystallized entries write into shared/ or work/, tagged with device_id=macbook-air-life
  3. After syncing to the work machine, its PG index includes “work fragments captured on the personal machine”
  4. Personal apps (private chat, music) still write only into personal/, never into shared/
Rule Description
personal/ is not synced to the work machine by default Requires explicit opt-in
work/ can be synced to the personal machine Convenient for checking project knowledge from home
Chat observation is filtered by context Personal-machine chat apps are never mixed with work-machine chat apps
PII is redacted before ingest Only already-redacted OKF is transmitted during sync

flowchart TD
    subgraph OLD["Old machine"]
        EXP["yzos export migrate-bundle<br/>OKF + overrides + sync.toml + device.json"]
    end
    EXP --> BUNDLE["yzos-migrate.tar.zst<br/>excludes PG / screenshots / voice / models"]
    BUNDLE --> IMP
    subgraph NEW["New machine"]
        IMP["yzos import migrate-bundle"] --> START["yzos daemon start<br/>PG init + reconcile"]
        START --> REBUILD["yzos index rebuild from OKF"]
        REBUILD --> MODELS["paths.models fetched locally"]
        MODELS --> MCP["configure MCP"]
    end
    MCP --> AI["upper-layer AI keeps working"]
yzos-migrate.tar.zst
-> contexts/ (full L1 OKF: default / work / personal / shared)
-> default/
-> knowledge/
-> memory/
-> entities/
-> shared/
-> entity_overrides/ (L2)
-> voiceprints/ (optional)
-> config.yaml (non-secret; includes the paths template, so the new machine can relocate models/observations)
-> sync.toml
-> device.json
-> MANIFEST.json (version, per-file hash manifest, okf_version)

Not included: the contents of paths.models, paths.database, or paths.observations – the new machine creates its own directories locally based on config.yaml’s paths, runs yzos models fetch to download weights, and rebuilds the PG index from an empty database via reconcile.

The new machine’s PG starts from an empty database; the scheduled reconcile and embedding_refresh build the index from the full OKF set. Duration depends on the amount of knowledge (roughly a few minutes for 10,000 OKF entries).

  • Keep it running: the old machine’s observation continues as a second node with a different device_id
  • Retire it: yzos device retire marks the device offline; entity metadata keeps the historical devices_seen record

Scenario 3: Heavy Multi-Device Users (e.g. Finance)

Section titled “Scenario 3: Heavy Multi-Device Users (e.g. Finance)”
flowchart TD
    HUB["Sync Hub - self-hosted, Git or S3"]
    HUB <-->|writes only to desk-a/| DA["Trading desk A"]
    HUB <-->|writes only to desk-b/| DB["Trading desk B"]
    HUB <-->|reads and writes shared/| OFF["Office"]

Each device:

  • Observes independently (L4 never crosses machines)
  • Writes into its own partition (desk-a/, desk-b/, shared/)
  • Pulls/pushes OKF to the Hub on a schedule
  • Indexes locally in PG, covering both its own L1 and any L1 already synced in

To avoid two machines independently discovering “Example Project” and generating two different IDs:

entity_id = {kind}-{stable_key}
stable_key generation rules:
person: hash(platform + platform_id) or manual_uuid
project: hash(normalized_repo_path) e.g. project-sha256(/path/to/repo)
task: hash(project_id + task_fingerprint) or ULID (requires a sync protocol)

Two machines that reference the same repo path generate the same project ID, so entries merge automatically on sync instead of duplicating.

File-level OKF conflicts (rare, since files are split per entity):

Strategy Applies to
Latest file mtime Default, simplest
Device priority Finance: trading desk outranks laptop
Three-way merge An entity’s summary field: both revisions are kept, a scheduled job synthesizes them with a model
Manual arbitration needs_review=true -> shown as a conflict in the Tauri UI

entity_revisions records every device’s every change, so conflicts can be traced back.

-- entity_revisions extension
device_id TEXT NOT NULL,
sync_gen BIGINT NOT NULL, -- sync generation, prevents replay
  • Every cross-device write is tagged with device_id and sync_gen
  • The Hub can optionally keep an append-only log (who pushed what, and when)
  • Raw observation (L4) never leaves its originating machine, satisfying compliance requirements

Regardless of whether the backend is Git or Syncthing, YZOS applies one uniform abstraction:

{
"device_id": "macbook-pro-work-7f3a",
"device_label": "Work MacBook Pro",
"context": "work",
"created_at": "2026-01-15T00:00:00Z",
"public_key": "..."
}
[sync]
enabled = true
mode = "active" # passive | active
backend = "rsync" # passive | rsync | webdav | s3 | git | rclone
interval_minutes = 15
[sync.scopes]
push = ["shared/**", "work/knowledge/**", "work/entities/**"]
pull = ["shared/**", "work/**"]
exclude = ["personal/**", "activity/**", "voice/**", "pg/**"]
[sync.conflict]
strategy = "mtime" # mtime | priority | merge | manual
device_priority = ["macbook-pro-work", "macbook-air-life"]
# pick one backend:
[sync.backend.rsync]
remote = "user@nas.local:/volume1/yzos-sync"
# [sync.backend.webdav]
# url = "https://cloud.example.com/dav/yzos-sync"
# auth = "keychain:yzos-webdav"
# [sync.backend.s3]
# endpoint = "https://s3.amazonaws.com"
# bucket = "yzos-sync"
# prefix = "shared/"
# auth = "keychain:yzos-s3"
# passive mode example:
# [sync]
# mode = "passive"
# watch_dir = "~/Library/Mobile Documents/com~apple~CloudDocs/yzos-sync"
flowchart TD
    P1["1. pull remote OKF changes"] --> P2["2. merge at file level"]
    P2 --> P3["3. yzos reconcile - OKF hashes vs local PG"]
    P3 --> P4["4. yzos index rebuild - changed files only"]
    P4 --> P5["5. entity_merge - shared-ID sightings"]
    P5 --> P6["6. push local OKF changes"]
    P6 --> P7["7. update sync_state - last_sync_at, sync_gen++"]
    P7 -.->|next interval| P1
Terminal window
yzos sync status # backend type, last sync time, pending push/pull file counts
yzos sync pull # pull via the configured backend (rsync/webdav/s3/git)
yzos sync push # push local changes
yzos sync reconcile # OKF <-> PG reconciliation only (common in passive mode)
yzos sync test # test connectivity (SSH/WebDAV/S3)
yzos sync backend list # available backends: rsync webdav s3 git rclone passive
yzos sync config init # interactively generate sync.toml
yzos export migrate-bundle -o ~/Desktop/yzos-migrate.tar.zst
yzos import migrate-bundle ~/Desktop/yzos-migrate.tar.zst
yzos device list
yzos device retire <device_id>
~/.yzos/.sync_state/
-> manifest.json (all files in this machine's scope: path + blake3 hash + mtime)
-> last_pull.json
-> last_push.json
-> conflicts/ (copies of unresolved conflicts)
-> shared/entities/project/foo.md.device-B

Scenario Topology Recommended backend Mode
Work + personal, 2 Macs Peer-to-peer, dual machine iCloud, sync into shared/ passive
Home NAS + laptop Star rsync + SSH to the NAS active
Self-hosted Nextcloud Hub WebDAV active
Multiple machines, no public IP P2P Syncthing (user-configured) + YZOS passive passive
Finance / multiple trading desks Hub + Spoke S3 / MinIO or a private Git repo active
Developers Any Git push to a knowledge repo active
Replacing a machine One-off migrate-bundle, or rsync to pull the full set
Fully offline, single machine Sync disabled

Recommended context split: work + personal + shared (see Scenario 1).


Once devices are aligned, the upper-layer AI (Claude Code) queries through MCP on any machine:

  • What it sees is the local PG index, backed by the already-synced L1 OKF
  • yzos_entity_get returns devices_seen, indicating which machine each piece of knowledge came from
  • Claude Code running on the personal machine can query Knowledge crystallized on the work machine, as long as it falls inside the sync scope

YZOS does not sync the upper-layer AI’s conversations – those live in each device’s own Claude Code session directory. YZOS only syncs the knowledge it crystallizes itself.


Phase Capability
P0 single machine OKF source of truth + PG rebuild + device_id reserved
P1 migration export/import migrate-bundle
P2 passive watch_dir + FSEvents + reconcile (for iCloud / Syncthing users)
P3 active rsync+SSH -> WebDAV -> S3 -> Git (implemented in this priority order)
P4 multi-device stable entity IDs + manifest optimistic locking + conflict UI
P5 rclone delegation, audit logging, optional end-to-end encryption layer

The single-machine phase reserves the sync.toml schema and .sync_state/; backend implementations can ship incrementally. Passive mode with iCloud has the lowest cost, so it is recommended as the first target for P2.

yzos-core/src/sync/
-> orchestrator.rs (pull -> merge -> reconcile -> push flow)
-> scope.rs (glob filtering for push/pull/exclude)
-> manifest.rs (blake3 hash manifest)
-> conflict.rs (mtime / priority / merge)
-> passive.rs (FSEvents watcher)
-> backends/
-> rsync.rs (invokes rsync over SSH)
-> webdav.rs
-> s3.rs (aws-sdk-s3 or rust-s3)
-> git.rs (invokes the git CLI)
-> rclone.rs (optional, invokes rclone)