Summary
Encrypted sync (Settings → Sync, Automerge vault) fails on every attempt with:
Attempting to change an outdated document.
Use Automerge.clone() if you wish to make a writable copy.
The failure happens after the sync engine has fully initialized (WASM loaded, credentials accepted, connection test passes) and after the first remote list request of each sync round returns successfully. No put is ever performed, so nothing is ever written to the remote backend — even with a brand-new, completely empty vault and a brand-new browser profile.
Steps to reproduce
- Deploy
ghcr.io/u14app/neo-chat:v2.4.0 via Docker Compose behind a reverse proxy (see Environment).
- Open the app in a fresh Chrome incognito profile (no prior local state, no IndexedDB).
- Settings → Sync: configure an S3-compatible backend (Cloudflare R2), fill endpoint/bucket/credentials. Test connection succeeds.
- Create a new vault, store the recovery code, click Sync now.
- The sync status shows the error above. Every subsequent retry (the scheduler re-triggers ~every 5 s) fails identically.
Expected behavior
A first sync with a fresh, empty vault completes: local state is merged and this device's encrypted snapshot is uploaded, so the remote bucket contains at least one object.
Actual behavior
Every round issues only the initial remote list request and then aborts locally with the Automerge error. No put is ever issued. Verified server-side: the reverse-proxy access log shows a single POST /api/sync/remote returning 200 per round and nothing else, and the R2 bucket stays at 0 objects / 0 B after dozens of attempts. Meanwhile the UI displays "Last sync ..." with a transfer size, which contradicts the server-side reality.
Also tried, same deterministic failure: deleting local chats, reloading the page, and the fresh incognito profile described above. The failure therefore does not depend on pre-existing local data or on a specific provider (the R2 path is verified end-to-end; the WebDAV path enters the same engine but was blocked earlier by a separate 401 credential/config issue in my environment, so it was not verified end-to-end there).
Environment
Browser: Chrome 151 (macOS 15, tested both normal and incognito profile)
OS: macOS 15 (client), Linux Mint (server)
Deployment: Docker (ghcr.io/u14app/neo-chat:v2.4.0) behind nginx reverse proxy + Cloudflare Tunnel
Node.js / pnpm: n/a (official Docker image)
Logs or screenshots
Reverse-proxy access log (client IP and host redacted), one representative sync round:
"POST /api/sync/remote HTTP/1.1" 200 35
The 35-byte response recurs every ~5 s; no request with a larger response body ever occurs (i.e. no snapshot put, no data get). The server-side /api/sync/remote route logs no error for these rounds — the failure is client-side, thrown after the list request succeeds. Remote backend (R2): 0 objects, 0 bytes.
Investigation leads (not confirmed root cause)
No browser stack trace captured yet; these are code-reading leads only:
src/lib/sync/engine.ts: mergeCapturedDocument(), applyLocalPayload(), mergeSyncDocuments() (implemented as api.merge(local, api.clone(remote))), addInitialLocalPayload()
src/lib/sync/crdt.ts
Automerge semantics: after merge, the merged-into document should not be changed directly again (clone it first). The observed error is consistent with a stale document handle being changed after an await boundary, but the exact document and call site are not yet pinned down. Happy to help gather more data on request.
Summary
Encrypted sync (Settings → Sync, Automerge vault) fails on every attempt with:
The failure happens after the sync engine has fully initialized (WASM loaded, credentials accepted, connection test passes) and after the first remote
listrequest of each sync round returns successfully. Noputis ever performed, so nothing is ever written to the remote backend — even with a brand-new, completely empty vault and a brand-new browser profile.Steps to reproduce
ghcr.io/u14app/neo-chat:v2.4.0via Docker Compose behind a reverse proxy (see Environment).Expected behavior
A first sync with a fresh, empty vault completes: local state is merged and this device's encrypted snapshot is uploaded, so the remote bucket contains at least one object.
Actual behavior
Every round issues only the initial remote
listrequest and then aborts locally with the Automerge error. Noputis ever issued. Verified server-side: the reverse-proxy access log shows a singlePOST /api/sync/remotereturning 200 per round and nothing else, and the R2 bucket stays at 0 objects / 0 B after dozens of attempts. Meanwhile the UI displays "Last sync ..." with a transfer size, which contradicts the server-side reality.Also tried, same deterministic failure: deleting local chats, reloading the page, and the fresh incognito profile described above. The failure therefore does not depend on pre-existing local data or on a specific provider (the R2 path is verified end-to-end; the WebDAV path enters the same engine but was blocked earlier by a separate 401 credential/config issue in my environment, so it was not verified end-to-end there).
Environment
Browser: Chrome 151 (macOS 15, tested both normal and incognito profile)
OS: macOS 15 (client), Linux Mint (server)
Deployment: Docker (
ghcr.io/u14app/neo-chat:v2.4.0) behind nginx reverse proxy + Cloudflare TunnelNode.js / pnpm: n/a (official Docker image)
Logs or screenshots
Reverse-proxy access log (client IP and host redacted), one representative sync round:
The 35-byte response recurs every ~5 s; no request with a larger response body ever occurs (i.e. no snapshot
put, no dataget). The server-side/api/sync/remoteroute logs no error for these rounds — the failure is client-side, thrown after the list request succeeds. Remote backend (R2): 0 objects, 0 bytes.Investigation leads (not confirmed root cause)
No browser stack trace captured yet; these are code-reading leads only:
src/lib/sync/engine.ts:mergeCapturedDocument(),applyLocalPayload(),mergeSyncDocuments()(implemented asapi.merge(local, api.clone(remote))),addInitialLocalPayload()src/lib/sync/crdt.tsAutomerge semantics: after
merge, the merged-into document should not bechanged directly again (clone it first). The observed error is consistent with a stale document handle being changed after anawaitboundary, but the exact document and call site are not yet pinned down. Happy to help gather more data on request.