Skip to content

Commit fe9caa5

Browse files
thedotmackclaude
andcommitted
docs(sync) + chore: final verification, docs, and stale-bundle fix (Phase 6)
Full-matrix e2e harness (scripts/sync-matrix-e2e.ts, 39/39): fresh-device bootstrap, >500-op offline catch-up, concurrent writes, all four mutation types in both arrival orders, epoch reset with native-corpus re-push, and kill-switch degradation — plus a two-real-daemon run proving the session-start context-inject pull end-to-end. docs/public/cloud-sync.mdx rewritten for the two-lane architecture (durable HTTP lanes, advisory WebSocket, poll-mode fallback, mutation convergence, settings reference) with availability framed honestly — no live hub URLs. Canary header comment scoped to what it measures. plugin/scripts/worker-service.cjs regenerated — the Phase 5 commit had shipped it stale (missing pollModeOnly/onSyncModeHint/X-Sync-Mode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 528f133 commit fe9caa5

4 files changed

Lines changed: 704 additions & 193 deletions

File tree

docs/public/cloud-sync.mdx

Lines changed: 111 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,130 @@
11
---
22
title: "Cloud Sync (cmem.ai Pro)"
3-
description: "Worker-native sync of your memory database to your cmem.ai account — no daemon, the worker syncs on write."
3+
description: "Two-lane multi-device sync of your memory database — durable HTTP push/pull through a per-user sync hub, with an optional WebSocket speed layer."
44
---
55

66
# Cloud Sync (cmem.ai Pro)
77

8-
Cloud sync backs up your local memory database to your [cmem.ai](https://cmem.ai)
9-
account. It is built into the worker: **there is no daemon — the worker syncs on
10-
write**. The same process that records every observation also uploads it, so
11-
there is no second process to install, monitor, or restart.
8+
Cloud sync replicates your local memory database across your devices through a
9+
**per-user sync hub** — an ordered log of everything your devices write. It is
10+
built into the worker: **there is no daemon — the worker syncs on write**. The
11+
same process that records every observation also uploads it, and pulls the
12+
other devices' writes back down.
1213

1314
<Note>
14-
**The database is the queue.** Every synced table carries a `synced_at` column
15-
(`NULL` = not in the cloud yet). After each write, the worker nudges a debounced
16-
flusher that drains `WHERE synced_at IS NULL` in batches to cmem.ai and stamps
17-
rows on success. That one mechanism **is** live sync, backfill, offline
18-
catch-up, and retry — a never-synced install simply has everything `NULL`, and
19-
anything that fails to upload stays `NULL` until a later flush picks it up.
15+
**Two lanes, one source of truth.** Everything durable travels over plain
16+
HTTP: pushes append to the hub's ordered log, pulls page through it with a
17+
cursor. An optional WebSocket rides alongside as a pure *speed layer* — it
18+
only makes the next pull happen sooner. The socket can be dropped, disabled,
19+
or wrong with **zero data loss**; the HTTP cursor is always the truth.
2020
</Note>
2121

2222
## What syncs
2323

24-
Three kinds of rows are uploaded to your cmem.ai account:
24+
Three kinds of rows are replicated between your devices:
2525

2626
- **Observations** — the compressed memories claude-mem generates from your sessions.
2727
- **Session summaries** — the per-session overview records.
28-
- **User prompts** — the prompts you typed (clamped to 200&nbsp;KB each).
28+
- **User prompts** — the prompts you typed (clamped to 200&nbsp;KB per field).
29+
30+
Alongside the rows, three kinds of **mutations** travel through the same log
31+
so later edits converge everywhere: session **title** changes, **prompt→session
32+
repairs** (a prompt captured before its session registered is re-linked on
33+
every device), and **project remaps** (worktree adoption and cwd-based moves
34+
retarget the affected rows on every device).
2935

3036
<Warning>
31-
**Privacy:** cloud sync uploads your observation narratives and your full prompt
32-
text to your cmem.ai account. Don't enable it if that content must stay on your
33-
machine.
37+
**Privacy:** cloud sync uploads your observation narratives and your full
38+
prompt text to the sync hub under your cmem.ai account. Don't enable it if
39+
that content must stay on your machine.
3440
</Warning>
3541

36-
## Quick start
37-
38-
Run the `/cloud-sync` skill in Claude Code. It checks sync status and, on first
39-
run, walks you through setup:
42+
## How the push lane works
4043

41-
1. Grab your **sync token** and **user id** from **cmem.ai → Connect** and paste
42-
them when asked. The skill writes them into `~/.claude-mem/settings.json`
43-
(mode `0600`) without ever echoing the token.
44-
2. The skill restarts the worker, which immediately starts draining unsynced
45-
rows, and polls status until the pending counts fall.
46-
47-
If you previously used the **legacy standalone sync client**, the skill migrates
48-
it automatically: your token is read from the old `.cloud-sync.env`, your device
49-
identity carries over, and **nothing re-uploads** — rows the old client already
50-
pushed are stamped as synced during migration. See
51-
[Migrating from the standalone client](#migrating-from-the-standalone-client).
52-
53-
## How the flusher behaves
44+
**The database is the queue.** Every synced table carries a `synced_at` column
45+
(`NULL` = not in the log yet). After each write, the worker nudges a debounced
46+
flusher that drains `WHERE synced_at IS NULL` and stamps rows on success. That
47+
one mechanism **is** live sync, backfill, offline catch-up, and retry — a
48+
never-synced install simply has everything `NULL`, and anything that fails to
49+
upload stays `NULL` until a later flush picks it up.
5450

5551
- **Debounced:** write bursts coalesce; the flusher runs ~1.5&nbsp;s after the
56-
last write, and only one flush runs at a time.
57-
- **Batched:** rows drain in batches of up to 200 per kind, packed into request
58-
bodies of at most 2&nbsp;MB.
52+
last write (250&nbsp;ms while the speed layer is connected), and only one
53+
flush runs at a time.
54+
- **Batched:** ops drain in requests of up to 500 ops / 2&nbsp;MB.
5955
- **Timeboxed:** every request has a 30&nbsp;s timeout — a dead network can
6056
never hang the worker.
61-
- **Retrying:** a failed upload leaves rows `NULL` and retries on the next write
62-
plus a capped exponential backoff (30&nbsp;s → 10&nbsp;min). The write path is
63-
never blocked — sync failures cost nothing but delay.
64-
- **Startup drain:** the worker kicks one flush at startup, which doubles as the
65-
initial backfill of your existing database.
57+
- **Retrying:** a failed upload leaves rows `NULL` and retries on the next
58+
write plus a capped exponential backoff (30&nbsp;s → 10&nbsp;min). The write
59+
path is never blocked — sync failures cost nothing but delay.
60+
- **Idempotent:** the hub dedupes on origin identity, so a retried push can
61+
never create duplicates.
62+
63+
## How the pull lane works
64+
65+
Each device keeps a **cursor** into the hub's log and pulls everything after
66+
it — in pages, so a device that was offline for a week (or is brand new and
67+
starts from zero) catches up the same way an active one stays current:
68+
69+
- **Session start:** the worker pulls immediately before injecting context
70+
(bounded to 1.5&nbsp;s, so a dead network can't stall your session), which
71+
means memory written on another machine is there the moment you start
72+
working.
73+
- **Steady state:** a short poll every 30&nbsp;s while a session is active,
74+
every 5&nbsp;min when idle, suspended entirely after an hour of no activity.
75+
Every push response also reports the log head, so an active device learns
76+
about remote writes without waiting out the timer.
77+
- **Applied like native writes:** pulled rows keep their original timestamps,
78+
carry their origin device, index into full-text and vector search through
79+
the same paths native writes use, and are stamped so they can never be
80+
echoed back up.
81+
82+
## The speed layer (optional WebSocket)
83+
84+
When enabled (the default), the worker also holds one WebSocket to the hub and
85+
receives new ops the moment another device pushes them — multi-device
86+
convergence in well under a second instead of a poll interval. The socket is
87+
strictly **advisory**: any anomaly (a gap, a parse error, a dropped
88+
connection) simply closes it and runs one HTTP pull, which is always correct.
89+
Set `CLAUDE_MEM_CLOUD_SYNC_WS` to `false` to turn it off — sync stays fully
90+
correct at poll latency.
91+
92+
The hub can also ask every client to fall back to polling by stamping
93+
responses with an `X-Sync-Mode: poll` header (an operational brake on the
94+
server side). Clients close their sockets and keep syncing over HTTP —
95+
degraded means *slower*, never *stopped* — and resume the socket automatically
96+
when the header disappears.
97+
98+
## Requirements and setup
99+
100+
Cloud sync is active when **all three** of the token, the user id, and the hub
101+
URL are non-empty — there is no separate enable flag; blank any one of them to
102+
turn sync off.
103+
104+
1. A **cmem.ai Pro sync token and user id** (from **cmem.ai → Connect**).
105+
2. A **sync hub URL** in `CLAUDE_MEM_CLOUD_SYNC_HUB_URL`. The hub is a
106+
Cloudflare Workers service (source in `workers/sync-hub/` with its own
107+
deploy runbook); use the hub URL your cmem.ai account documentation
108+
provides for it.
109+
110+
Run the `/cloud-sync` skill in Claude Code to check status and walk through
111+
setup. It writes the settings into `~/.claude-mem/settings.json` (mode
112+
`0600`) without ever echoing the token, restarts the worker, and polls status
113+
until the pending counts fall. If you previously used the legacy standalone
114+
sync client, the skill retires it and carries your device identity over — see
115+
[Migrating from the standalone client](#migrating-from-the-standalone-client).
66116

67117
## Settings
68118

69-
Cloud sync is configured in `~/.claude-mem/settings.json`. It is **active when
70-
both the token and the user id are non-empty** — there is no separate enable
71-
flag; blank the token to turn sync off.
72-
73119
| Key | Default | Meaning |
74120
|-----|---------|---------|
75121
| `CLAUDE_MEM_CLOUD_SYNC_TOKEN` | `''` | Your cmem.ai sync token (from **cmem.ai → Connect**). Sent as `Authorization: Bearer`. |
76-
| `CLAUDE_MEM_CLOUD_SYNC_USER_ID` | `''` | Your cmem.ai user id (from **cmem.ai → Connect**). |
77-
| `CLAUDE_MEM_CLOUD_SYNC_URL` | `https://cmem.ai/api/pro/sync` | Sync endpoint base URL. Only change this if you're pointing at a non-production server. |
78-
| `CLAUDE_MEM_CLOUD_SYNC_DEVICE_ID` | `''` | Stable identity of this machine in the cloud. Resolved at first start — adopted from the legacy client's state file if present, otherwise a fresh UUID — then persisted back here. Don't edit it: the server keys rows on it, and a changed id forks every cloud row into a duplicate. |
79-
| `CLAUDE_MEM_CLOUD_SYNC_DEVICE_NAME` | machine hostname | Human-readable label for this device in the cmem.ai Devices panel. |
122+
| `CLAUDE_MEM_CLOUD_SYNC_USER_ID` | `''` | Your cmem.ai user id (from **cmem.ai → Connect**). All your devices share it — it names your hub log. |
123+
| `CLAUDE_MEM_CLOUD_SYNC_HUB_URL` | `''` | Sync hub base URL. **Empty = sync off.** |
124+
| `CLAUDE_MEM_CLOUD_SYNC_WS` | `'true'` | Advisory WebSocket speed layer. `'false'` = HTTP polling only; sync stays fully correct at poll latency. |
125+
| `CLAUDE_MEM_CLOUD_SYNC_DEVICE_ID` | `''` | Stable identity of this machine. Resolved at first start — adopted from the legacy client's state file if present, otherwise a fresh UUID — then persisted back here. Don't edit it: every row is attributed to its origin device, and a changed id forks every previously synced row into a duplicate. |
126+
| `CLAUDE_MEM_CLOUD_SYNC_DEVICE_NAME` | machine hostname | Human-readable label for this device. |
127+
| `CLAUDE_MEM_CLOUD_SYNC_URL` | (legacy) | The pre-hub per-kind endpoint base. Unused since the hub cutover; kept only so existing settings files round-trip. |
80128

81129
## Status endpoint
82130

@@ -95,14 +143,14 @@ Configured:
95143
{
96144
"configured": true,
97145
"deviceId": "2f6b1c9e-7d41-4c1a-9b0e-3d5f8a2c6e10",
98-
"pending": { "observations": 0, "summaries": 0, "prompts": 2 },
146+
"pending": { "observations": 0, "summaries": 0, "prompts": 2, "mutations": 0 },
99147
"lastFlushAt": 1783981042731,
100148
"lastError": null
101149
}
102150
```
103151

104-
- `pending` — rows per kind still waiting to upload (`synced_at IS NULL`).
105-
Counts near 0 mean the cloud copy is current.
152+
- `pending` — rows (and queued mutation ops) still waiting to upload. Counts
153+
near 0 mean the hub's log has everything this device wrote.
106154
- `lastFlushAt` — epoch ms of the last successful flush, `null` before the first.
107155
- `lastError` — message from the most recent failed flush, `null` when healthy.
108156
It never contains the token.
@@ -117,23 +165,22 @@ Not configured:
117165

118166
Before sync moved into the worker, cloud sync ran as a separate standalone
119167
client (`cloud-sync.mjs` plus a `.cloud-sync.env` credentials file and a
120-
`cloud-sync.pid` file). The worker supersedes it entirely — if the old client
121-
kept running it would double-upload — so the `/cloud-sync` skill retires it
122-
during setup:
168+
`cloud-sync.pid` file). The worker supersedes it entirely, so the
169+
`/cloud-sync` skill retires it during setup:
123170

124171
- **Stopped:** if `cloud-sync.pid` points at a live daemon, it is killed.
125172
- **Retired:** `cloud-sync.mjs`, `.cloud-sync.env`, and `cloud-sync.pid` are
126173
renamed with a `.retired` suffix (archived, not deleted). Your token is copied
127174
from `.cloud-sync.env` into `settings.json` first, so setup needs no re-pasting.
128-
- **Preserved:** `~/.claude-mem/cloud-sync-state.json` is deliberately **left in
129-
place**. The worker reads two things from it: its upload cursors (used once,
130-
by a database migration, to stamp everything the old client already pushed —
131-
this is why migration re-uploads nothing) and its `deviceId` (adopted into
132-
`CLAUDE_MEM_CLOUD_SYNC_DEVICE_ID` so the cloud sees the same device before and
133-
after). Renaming or deleting that file would mint a new device identity and
134-
fork every previously synced row into a duplicate.
175+
- **Preserved:** `~/.claude-mem/cloud-sync-state.json` is deliberately **left
176+
in place** — the worker adopts its `deviceId` into
177+
`CLAUDE_MEM_CLOUD_SYNC_DEVICE_ID` so this machine keeps one identity across
178+
the migration. Renaming or deleting that file before the first worker start
179+
would mint a new device identity and fork every previously synced row into a
180+
duplicate.
135181

136182
<Note>
137-
The standalone client remains available from cmem.ai as an out-of-repo utility
138-
for non-plugin users. The plugin no longer depends on it.
183+
On first contact with the hub, every device re-pushes its own corpus once
184+
(the hub's dedupe makes this safe) — that is how the shared log is built, and
185+
it doubles as the initial backfill of your existing database.
139186
</Note>

plugin/scripts/worker-service.cjs

Lines changed: 126 additions & 126 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)