Multi-Device Sync and Alignment
このコンテンツはまだ日本語訳がありません。
Full spec: 00-product-spec.md.
Problem Scenarios
Section titled “Problem Scenarios”| 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.
Design Principles
Section titled “Design Principles”1. Sync knowledge, not everything
Section titled “1. Sync knowledge, not everything”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.
2. OKF is a portable unit
Section titled “2. OKF is a portable unit”Every OKF file is an independent Markdown file with frontmatter, which naturally supports:
git push/pull- Syncthing / iCloud folder sync
- packaging with
yzos export --bundlefor migration
The PG index on any given device is only a local copy of the same set of OKF files.
3. Observed data carries device context
Section titled “3. Observed data carries device context”Entries that crystallize into the knowledge base must record their source device so conflicts can be resolved when merging:
# entity frontmatter extensiondevice_id: macbook-pro-workdevice_label: Work MacBookcontext: work # work | personal | travelfirst_seen_device: macbook-pro-workdevices_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:
- Defining the sync scope (which OKF directories participate in sync)
- Defining the alignment flow (pull -> merge -> reconcile -> push)
- 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"]
Sync Backends
Section titled “Sync Backends”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 = truemode = "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
reconcileand an incrementalindex 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.
Active - rsync over SSH
Section titled “Active - rsync over SSH”The user configures their own SSH target (home NAS, cloud host, office jump box):
[sync]enabled = truemode = "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 aliasssh_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:
# pullrsync -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)
Active - WebDAV
Section titled “Active - WebDAV”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 plaintextauth = "keychain:yzos-webdav" # or basic / digesttls_verify = truetimeout_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
Active - S3-compatible object storage
Section titled “Active - S3-compatible object storage”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:9000region = "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:
- pull: ListObjects -> compare against manifest -> download files whose hash differs
- merge: apply the local conflict strategy
- 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.
Active - Git
Section titled “Active - Git”[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
.mdmerge conflict is flaggedneeds_review - Works with private GitHub, Gitea, or self-hosted GitLab
Passive / existing setup - Syncthing
Section titled “Passive / existing setup - Syncthing”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.
Optional delegation - rclone
Section titled “Optional delegation - rclone”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.confA single rclone.conf can cover WebDAV, S3, Google Drive, and dozens of other backends; YZOS does not reimplement them.
Credentials and Security
Section titled “Credentials and Security”| 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)”Recommended: dual context with partial sharing
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):
contexttemporarily switches towork(orwork@personal-device)- New crystallized entries write into
shared/orwork/, tagged withdevice_id=macbook-air-life - After syncing to the work machine, its PG index includes “work fragments captured on the personal machine”
- Personal apps (private chat, music) still write only into
personal/, never intoshared/
Privacy firewall
Section titled “Privacy firewall”| 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 |
Scenario 2: Replacing a Machine
Section titled “Scenario 2: Replacing a Machine”Migration flow (5 steps)
Section titled “Migration flow (5 steps)”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"]
migrate-bundle contents
Section titled “migrate-bundle contents”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).
Handling the old machine
Section titled “Handling the old machine”- Keep it running: the old machine’s observation continues as a second node with a different
device_id - Retire it:
yzos device retiremarks the device offline; entity metadata keeps the historicaldevices_seenrecord
Scenario 3: Heavy Multi-Device Users (e.g. Finance)
Section titled “Scenario 3: Heavy Multi-Device Users (e.g. Finance)”Topology: Hub + Spoke (recommended)
Section titled “Topology: Hub + Spoke (recommended)”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
Entity IDs must be globally unique
Section titled “Entity IDs must be globally unique”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.
Conflict handling
Section titled “Conflict handling”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.
Auditing (finance scenario)
Section titled “Auditing (finance scenario)”-- entity_revisions extensiondevice_id TEXT NOT NULL,sync_gen BIGINT NOT NULL, -- sync generation, prevents replay- Every cross-device write is tagged with
device_idandsync_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
Sync Protocol (YZOS Built-in Layer)
Section titled “Sync Protocol (YZOS Built-in Layer)”Regardless of whether the backend is Git or Syncthing, YZOS applies one uniform abstraction:
device.json
Section titled “device.json”{ "device_id": "macbook-pro-work-7f3a", "device_label": "Work MacBook Pro", "context": "work", "created_at": "2026-01-15T00:00:00Z", "public_key": "..."}sync.toml (full example)
Section titled “sync.toml (full example)”[sync]enabled = truemode = "active" # passive | activebackend = "rsync" # passive | rsync | webdav | s3 | git | rcloneinterval_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 | manualdevice_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"Sync cycle
Section titled “Sync cycle”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
yzos sync status # backend type, last sync time, pending push/pull file countsyzos sync pull # pull via the configured backend (rsync/webdav/s3/git)yzos sync push # push local changesyzos 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 passiveyzos sync config init # interactively generate sync.toml
yzos export migrate-bundle -o ~/Desktop/yzos-migrate.tar.zstyzos import migrate-bundle ~/Desktop/yzos-migrate.tar.zstyzos device listyzos device retire <device_id>sync_state (local device state)
Section titled “sync_state (local device state)”~/.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-BRecommended Configuration per Scenario
Section titled “Recommended Configuration per Scenario”| 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).
Relationship with the Upper-Layer AI
Section titled “Relationship with the Upper-Layer AI”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_getreturnsdevices_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.
Implementation Priority
Section titled “Implementation Priority”| 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.
Rust module plan
Section titled “Rust module plan”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)