# Syncthing — VPS ↔ Connie continuous sync (Obsidian vault)

Session reference: set up 2026-07-19. Goal was a single-source-of-truth Obsidian vault on the VPS that Connie (and later a tablet) edit locally, with the agent able to read/write the same files on the VPS. Syncthing peer sync was chosen over remote-mount (sshfs/NFS = fragile over WAN) and over keeping the vault on Connie (agent can't reach it).

## Final state (don't rebuild — reuse)

- **Vault content:** `/root/.hermes/vault/` on the VPS. Folders: `01_Job_Seeker`, `02_Pipeline_Layer`, `03_Real_Results_Front_Desk`, `04_OriginatorOS`, `Personal`, `Jarvis`, `Agentic OS`, `Memories`. The older `/root/.hermes/Business_Projects/Project_*` content was merged in (Project_2→02, Project_3→03, Project_4→04, Session Handoffs→Memories); originals left in place.
- **Connie mirror:** `C:\Users\Rob\Documents\Obsidian Vault` (this exact path is what the Agentic OS dashboard's `vaultRoot` points to — keep it).
- **VPS device ID:** `7KUM336-4B22UB6-NIHR6TO-6M4ASSN-DUAQ5BQ-VGZXG2D-IZ7WMWR-SAMJHQL` (name `robshermes`)
- **Connie device ID:** `34F6KTU-6OWPYR4-UQB6UDU-OUHYJHQ-L7M537Q-BP534YN-T4XGDGU-HGIXNAZ` (name `Connie`)
- **Folder:** ID `hermes-vault`, label "Hermes Vault", type sendreceive, fsWatcher on.
- **Surface (tablet) device ID:** `B2RA3UL-MWDJM4Z-7UJZFWV-QI6I2QC-5HZEHZA-WYQHIB4-KWB3MWS-YJZD5QL` (name `Surface`), added 2026-07-19 as third device on `hermes-vault`. Local vault path `C:\Users\rkbla\Documents\Obsidian Vault` — note **the Surface's Windows username is `rkbla`, NOT `Rob`** — per-device usernames differ; never assume `C:\Users\Rob` on a new device, ask or check `$env:USERNAME` first.

## VPS install + service

```bash
DEBIAN_FRONTEND=noninteractive apt-get install -y syncthing   # v1.27.x
systemctl enable --now syncthing@root
```

- Config lands in `/root/.local/state/syncthing/config.xml` — **NOT** `~/.config/syncthing` (the XDG state dir). Searching the wrong path cost a step; check both.
- GUI listens on `127.0.0.1:8384`, sync on `*:22000`. Device ID: `grep -oP 'device id="\K[^"]+' <config.xml>` (first match). API key: `grep -oP 'apikey>\K[^<]+' <config.xml>`.

## Pairing a new device entirely over REST (no GUI needed on VPS)

```bash
API="<apikey from config.xml>"
# 1. add the peer device
curl -s -X POST -H "X-API-Key: $API" -H "Content-Type: application/json" \
  http://127.0.0.1:8384/rest/config/devices \
  -d '{"deviceID":"<PEER-ID>","name":"<name>","addresses":["dynamic"],"compression":"metadata","introducer":false,"paused":false}'
# 2. share the folder to both devices
curl -s -X POST -H "X-API-Key: $API" -H "Content-Type: application/json" \
  http://127.0.0.1:8384/rest/config/folders \
  -d '{"id":"hermes-vault","label":"Hermes Vault","path":"/root/.hermes/vault","type":"sendreceive","devices":[{"deviceID":"<PEER-ID>"},{"deviceID":"7KUM336-4B22UB6-NIHR6TO-6M4ASSN-DUAQ5BQ-VGZXG2D-IZ7WMWR-SAMJHQL"}],"rescanIntervalS":3600,"fsWatcherEnabled":true,"paused":false}'
```

The peer's GUI then pops "device wants to connect" + "wants to share folder" — user approves and sets the local path (`C:\Users\Rob\Documents\Obsidian Vault` for Connie). First sync follows in seconds.

### Adding a device to an EXISTING folder (third+ device) — PUT, not POST

POST-ing the folder again errors (ID exists). Instead GET the folder config, append the device, PUT it back:

```bash
API="<apikey>"
curl -s -H "X-API-Key: $API" "http://127.0.0.1:8384/rest/config/folders/hermes-vault" > /tmp/vf.json
python3 - << 'EOF'
import json
f = json.load(open('/tmp/vf.json'))
f['devices'].append({"deviceID":"<NEW-PEER-ID>","introducedBy":"","encryptionPassword":""})
json.dump(f, open('/tmp/vf2.json','w'))
EOF
curl -s -X PUT -H "X-API-Key: $API" -H "Content-Type: application/json" \
  http://127.0.0.1:8384/rest/config/folders/hermes-vault -d @/tmp/vf2.json
```

### Discovering incoming folder offers from a peer

When the PEER creates/shares a folder toward the VPS (e.g. Connie sharing `claude-code-sessions`), the VPS doesn't auto-accept — the offer sits pending. List it:

```bash
curl -s -H "X-API-Key: $API" http://127.0.0.1:8384/rest/cluster/pending/folders
# {"<folder-id>": {"offeredBy": {"<peer-id>": {"time":..., "label":...}}}}
```

Then accept by POST-ing the folder config with the SAME id the peer used (mismatch = two separate folders).

## Verify sync (REST, from VPS)

```bash
curl -s -H "X-API-Key: $API" "http://127.0.0.1:8384/rest/db/status?folder=hermes-vault"   # state=idle, needBytes=0
curl -s -H "X-API-Key: $API" "http://127.0.0.1:8384/rest/system/connections"             # peer connected=true, type=tcp-server
```

End-to-end proof pattern: write a test .md into `/root/.hermes/vault/<folder>/` with write_file, have the user confirm it appears in Obsidian on Connie. That validates the whole agent→VPS→sync→Obsidian loop at once.

## Connie (Windows) side

- Install: `winget install Syncthing.Syncthing`. **Pitfall:** the install modifies PATH but the *current* PowerShell window won't see `syncthing` — open a fresh window, or find the exe under `%LOCALAPPDATA%\Microsoft\WinGet\Packages\...`.
- Run manually: `syncthing` (blocks the window) or `Start-Process syncthing -WindowStyle Minimized` (detached). Windows firewall prompt: allow Private + Public — both fine (traffic is TLS + requires explicit device-ID pairing; randos can't connect).
- **Auto-start (bare winget build has no GUI 'start on login' toggle):** Scheduled Task, needs an **ADMIN** PowerShell (`Register-ScheduledTask` → Access denied otherwise):

```powershell
$exe = (Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter syncthing.exe | Select-Object -First 1).FullName
$action = New-ScheduledTaskAction -Execute $exe -Argument "--no-console --no-browser"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName "Syncthing" -Action $action -Trigger $trigger -Principal $principal -Force
```

Syncthing v2.x note: `--no-console` is the hidden flag (`--no-console` not `-no-console` alone; the v1 `--no-console` equivalent works). `--no-browser` stops the GUI popping every login; user opens `127.0.0.1:8384` manually when wanted.
- **Symptom:** `127.0.0.1:8384` 404s/refuses → syncthing process died (window closed / sleep). Check `Get-Process syncthing`, relaunch. Not a sync break — pairing and folder config persist in `%LOCALAPPDATA%\Syncthing` (or the config dir), restart resumes cleanly.

## Second folder live: claude-code-sessions (Connie→VPS receive-only)

Beyond the vault, a second Syncthing folder mirrors Claude Code session transcripts so the agent can read them for digest/curation: folder ID `claude-code-sessions`, source `C:\Users\Rob\.claude\projects` on Connie, lands at `/root/.hermes/mirrors/claude-code-sessions` on the VPS with `type: "receiveonly"` (VPS never writes back). Deliberately OUTSIDE the vault — raw transcripts are input to curation, not curated notes. When the user re-shares a folder with the SAME ID after removing it, the VPS side keeps its folder config and re-pairs without a new pending offer; if the index exchange wedges (see pitfalls), `systemctl restart syncthing@root` forces a fresh handshake.

## Vault merge procedure (one-time, done 2026-07-19 — reuse the pattern)

When adopting an existing local vault as the synced one: (1) build `/root/.hermes/vault/` with the local folder schema, (2) `cp -rn` (no-clobber) old VPS content into the matching folders, leave originals in place as backup until the user verifies, (3) share via Syncthing and point the Connie side at the EXISTING local vault path — Syncthing overlays and merges (different filenames = no conflicts; same-name-different-content = conflict flagged, never silently destroyed). Prove the merge by comparing total md-file counts on both ends (`Get-ChildItem -Recurse -Filter *.md` on Connie vs `find ... -name '*.md' | wc -l` on VPS — note non-md files like scripts/data make totals differ from md counts).

## Gotchas

- **Stuck at ~96% on a tiny first file = wedged index exchange.** Symptom: peer GUI shows "syncing 96%" forever, VPS `/rest/db/status` shows global=0/local=0, no pending offer, no error. Fix: `systemctl restart syncthing@root` on the VPS, then re-check `/rest/db/status` — it completes within seconds. Don't remove/re-add folders first; restart is the cheap fix.
- **Folder paths are LOCKED after creation in the GUI — typo = remove and re-add the folder.** A path saved as `...\.claude\project` (missing `s`) silently syncs an EMPTY folder (Syncthing creates the missing dir) while the real files sit untouched. Diagnosis: compare `/rest/db/status` globalFiles with the peer's GUI item count; one side showing "1 file, 41.3 KiB" and the other global=0 = wrong path or broken exchange. Re-adding with the SAME folder ID re-pairs cleanly; the stray empty dir on disk is harmless.
- **Verify a NEW folder share by announced item counts on BOTH sides before trusting it.** Counts must match after first sync; zero on the receiving side while the sender shows files means path/pairing/exchange trouble — check path first, then restart.
- Port 22000 TCP reachable = direct peer (`type=tcp-server` in connections, fast). If blocked it falls back to relays — works, slower. On this Hostinger VPS no change was needed.
- Windows `tar`/scp verification pitfalls (exit-code lies) still apply to one-shot transfers — but NOT to Syncthing, which has its own consistency model (global/local file counts in `/rest/db/status`).
- Obsidian's `.obsidian/` folder syncs too — that's desired here (keeps plugins/settings consistent), but be aware workspace layout conflicts can appear if both ends have Obsidian open simultaneously on different window layouts.
- The Agentic OS dashboard (`127.0.0.1:3737/memory`) reads the vault via its `vaultRoot` in `C:\Users\Rob\.agentic-os\config.json` — it stays pointed at the Connie path; the sync keeps it fresh.
