Skip to content

[Bug]: Encrypted sync fails with Automerge "outdated document" before any data is uploaded #201

Description

@JasonWenTheFox

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

  1. Deploy ghcr.io/u14app/neo-chat:v2.4.0 via Docker Compose behind a reverse proxy (see Environment).
  2. Open the app in a fresh Chrome incognito profile (no prior local state, no IndexedDB).
  3. Settings → Sync: configure an S3-compatible backend (Cloudflare R2), fill endpoint/bucket/credentials. Test connection succeeds.
  4. Create a new vault, store the recovery code, click Sync now.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions