Skip to content
Download

Peer Sync & Visualization

Memory and graph origins survive sync; imported paths are references until mapped on the receiver. Device identity does not partition shared knowledge or change vectors. Local identity/root mappings are not transferred. See devices and portable project paths.

Live sync copies one workspace between two machines through a relay that only pairs them by room and forwards frames. The serving machine prints a one-time share code; the other machine passes it to imprint sync. Sessions are end-to-end encrypted with the code’s secret part, so the relay — including the default public one — never sees it, the devices or the memories. Joining only receives unless you add --two-way or --send-only, and then you see which machine gets your workspace and confirm before anything is sent.

There is one implementation, imprint/sync_ws.py (async-generator sessions that yield events). The CLI renders them in a terminal (imprint/cli_sync.py; imprint sync is a thin Go wrapper that runs it), and the dashboard streams the same events over SSE. The relay itself is Go (cmd/relay.go). See ADR 0018.

sequenceDiagram
participant A as Machine A (serving)
participant R as Relay
participant B as Machine B (joining)

A->>R: connect (room=7f3a9c21, role=provider)
Note over A: imprint sync serve [--workspace W] [--send-only or --receive-only]<br/>share code 7f3a9c21-PIN, workspace and mode shown on screen
B->>R: connect (room=7f3a9c21, role=consumer)
Note over B: imprint sync 7f3a9c21-PIN [--workspace W] [--two-way or --send-only]

B->>R: PAKE (SPAKE2 message bound to the room)
R->>A: forward
A-->>R: PAKE (SPAKE2 message)
R-->>B: forward
Note over A,B: both derive the session key from the PIN<br/>every later frame is AES-256-GCM, unreadable to the relay

B->>R: sealed HELLO (hostname, user, os, fingerprint)
R->>A: forward
Note over A: cannot open it: wrong PIN, session ends<br/>trusted fingerprint: accept, else ask [y/n/t]
A-->>R: sealed 200 + grant {pull, push} + A's device, or 403
R-->>B: forward
Note over B: plan = grant AND own policy (receive-only by default)<br/>push planned: show A's device, ask [y/N]<br/>no phase left: fail with nothing to sync

opt plan includes pull (A sends)
B->>R: sealed GET /sync/pull
A-->>R: sealed meta, batch x N, done
Note over B: store precomputed vectors and facts, deduplicated
end

opt plan includes push (A imports)
B->>R: sealed POST /sync/push, then meta, batch x N, done
Note over A: store precomputed vectors and facts, deduplicated
A-->>R: sealed ack {status 200, body: stats}
end

Note over A,B: a request outside the grant gets 403 and ends the session<br/>a frame that fails authentication ends the session, nothing from it is stored

The public relay at wss://sync.alexandruleca.com is the default — pass --relay to use your own. The relay pairs one provider and one consumer per room and refuses a second connection for a taken role, caps messages at 80 MiB, and closes rooms still waiting for their second peer after 1 hour (a paired transfer runs as long as it needs). Frames a peer sends before the other one joins are held, up to 1 MiB, and delivered first when it joins, so paired devices that reconnect at the same moment can arrive in either order. Relays before v0.20.1 dropped those frames: when the consumer joined first, its key exchange was lost and automatic sync failed with “The other machine did not answer the key exchange” until one side’s timing changed. Only WebSocket requests reach rooms, so protocol errors go to sync clients alone; a browser on any address of the relay gets a static page that explains it (cmd/relay.html, 404 off the root). The page has no scripts or third-party requests unless the operator starts the relay with --ga4-id G-…, which adds Google Analytics page views reported as / or /not-found, never the address itself, since a path may carry a share code. GET /health returns {"status","version","rooms","sessions","since"}: open rooms, rooms both peers joined since the relay started (paired devices reconnect about once a minute, and each reconnect counts), and that start time. Counters live in memory and reset on restart. Any origin may read it, so the website can show the public relay’s status.

What transfers (from the selected workspace only):

  • Qdrant points — content, metadata and the stored vector, streamed in batches. Each record carries the sender’s embedding_identity; the importer rejects a missing or mismatched identity before any upsert, so both machines need the same embedding profile (see embedding profile compatibility). Imports dedup by the record’s logical ID.
  • Tags follow the latest retag. A memory the receiver already has keeps its content and vector, but takes the sender’s tags, type and LLM-tagged flag when they were set more recently (tagged_at, stamped by imprint retag and background LLM tagging). So a memory created on one machine and retagged on the other gets the new tags on both. Ties keep the local tags. A retag always stamps past the tags it replaced, so the order holds even when the devices’ clocks disagree. Memories retagged before this existed carry no stamp and stay as they are until their next retag (ADR 0051).
  • Complete redacted source snapshots and character-span manifests, as bounded compressed checksummed frames after the point batches. Only complete validated transfers publish. Newer source update times win for heads; points still merge without deletions. Purge exclusions and project placement apply. Snapshot bundles include source-snapshots.jsonl. See source memory for version compatibility and clock-skew limits.
  • Knowledge graph facts (SQLite facts table) — streamed in batches, deduped by (subject, predicate, object, valid_from).
  • Project placement rules (moves), with when each last changed and moves back as removed rules, to a peer that offers them. The receiver takes a rule newer than its own and moves its own copies of the memories it governs, as a move made there; when a job holds the queue, an apply-placements job does it next. Rules stream before memories, so memories that follow are placed by them (ADR 0134).
  • Organize’s proposals and decisions, newest change winning, to a peer that offers them, with quotes and previews redacted again on arrival.
  • Memories that wait for LLM tagging, to a peer that offers them: the receiver marks those it has not classified as waiting too, so a device that runs LLM tagging tags them, and their tags come back like any classification. Each sync also says whether the device runs LLM tagging; with tagger.llm off and tagger.peer on (the default), a device paired with one that tags marks its new memories for it and never loads a tagging model itself (ADR 0138).
  • Not transferred: other workspaces, the WAL, config, the trust list, workspace roots and purge exclusions.
  • Moved memories travel with their origin project, and each device computes IDs from its own placement. A peer on an older version gets no rules and keeps its own layout; copies dedupe instead of undoing a move (ADR 0036).
  • Imported records are evidence, not authority: imprint refresh-urls (and the MCP refresh) re-fetch only URLs this device recorded, never URLs that arrived through sync.

Core memories (ADR 0129), guidelines and records alike (ADR 0143), belong to no workspace, so every paired workspace’s sync carries them. Each is one record, and the newest change wins: approving, retiring or editing one on any device holds on all of them. A device sends them only to a peer whose handshake offers them and says it tells guidelines from records (core_tiers); a release before that would give every record to its sessions, so it is sent none, though its own still arrive and take the tier their origin gives. Only the device that runs LLM tagging for a pair drafts guidelines in the background, so two devices do not propose the same ones. Meeting recordings’ transcripts sync as ordinary memories; their audio syncs only where both devices turn it on (below).

Recordings stay on the device that made them unless Settings → Device sync → Sync recordings (sync.recordings, off by default) is on at both ends (ADR 0177). Then each finished recording, its audio, details, transcript and speakers’ names, reaches the other device by itself, end-to-end encrypted like the rest of sync; the transcript’s memories sync either way, as memories. The audio goes in 1 MiB pieces, each stored and acknowledged before the next, into a partial file that becomes the recording only once its size and SHA-256 match, so an interrupted transfer resumes where it stopped, and audio the other device already holds is not sent again. Deleting a recording, or only its audio, on one device does the same on the other. A recording larger than audio.max_file_size_mb is not received. Audio from a release that did not yet give it its length and seek index is given them on arrival, and that newer version goes back to the sender (ADR 0184); audio that cannot replace the copy held, as when Windows has it open, is sent again by the next sync. The relay forwards ciphertext and keeps nothing, but sees how much passes and when, so it can tell roughly how long a recording is.

Skills and personas (ADR 0168) also belong to no workspace, and travel the same way: one record per item, the newest change wins, sent only to a peer whose handshake offers them. The version a received change replaces stays in the receiving device’s history, which never syncs. A device writes the items it receives into its own clients’ folders at once (ADR 0169); see skills and personas.

A one-time sync stays one-time unless both devices enable automatic pairing. In the dashboard, check Enable automatic sync with this device after pairing on Serve and Receive before starting. Choose the workspace and direction first; joining still defaults to receive-only. The existing approval and send confirmation apply. A declined push is never remembered as permission to send. The success note confirms when pairing has completed.

Terminal window
# Device A: offer pairing for this workspace
imprint sync serve --workspace team --remember
# Device B: receive and send, after confirmation; workspace names may differ
imprint sync <code> --workspace team-local --two-way --remember
# Keep this running on each device, or leave each dashboard/backend running
imprint sync watch

The backend reconnects through the same relay using a new, private pair capability, not the old share PIN or a device fingerprint. It verifies the paired workspace mapping and only runs the approved directions. About once a minute, when both workers can connect, each device lists what sync can change without reading vectors (each memory with its classification stamp, each fact, each source snapshot with its update time), the devices swap these lists, and each sends only what the other lacks or holds an older version of. An unchanged workspace sends nothing else. The records travel in chunks of up to 200 memories (with their vectors) or facts, or one source snapshot, and the receiver stores and acknowledges each before the next, so an interrupted sync keeps what arrived and the next attempt sends the rest. The sender prepares the next chunk while the other device stores the last (ADR 0123). An attempt runs as long as it makes progress; it stops after 10 minutes without any. Both devices need the same release for this; a device on an older one gets and sends whole workspaces, as before (ADR 0116). Received records are redacted again before they are stored; with redact.privacy_filter on, that takes about a second a memory on a CPU, so a first sync of a large workspace can store for hours, unless the sending device runs the filter on a GPU and marks what it sent (see secret redaction). A device storing a chunk says so every 30 seconds, which counts as progress, and both devices show storing meanwhile (ADR 0120). No embedding call is required. Turning off the app, network outages or an unavailable relay delay sync. watch is a foreground service command; pairing does not silently install a system startup service.

Sync → Paired devices shows the relay, local/remote workspace, direction, last completed sync with what it received and sent, when the devices last found nothing to change, and the last connection problem. While an attempt runs, the device shows what it is doing: waiting for the other device, comparing the workspaces, then sending, receiving or storing records, with how many of how many and a progress bar. Any process’s worker keeps this on the pairing record, so the dashboard shows it whether the dashboard or imprint sync watch runs the sync. The Sync page has a tab each for paired devices, live sync and snapshots; the address keeps the open one (/sync/#live). Each device’s actions menu (⋯) holds Pause automatic sync, which stops workspace transfers from either side, Resume automatic sync, which needs no new code, Forget on both devices… and Remove from this device…. CLI equivalents:

Terminal window
imprint sync peers
imprint sync disable <pair-id>
imprint sync enable <pair-id>
imprint sync forget <pair-id> # asks for confirmation; no implicit --yes
imprint sync remove <pair-id> # this device only, now; asks for confirmation

Forget on both devices… requires confirmation and affects all paired workspaces with that device. It revokes automatic transfer locally and removes its old fingerprint trust immediately. When both workers reconnect, an encrypted notification causes the other device to forget this one too. While it is offline, the interface says remote removal pending. Keep Imprint running on both to finish; the relay does not store an offline revocation mailbox. A lost final acknowledgement may leave a pending notification rather than a false success. Existing memories remain on both machines; forgetting cannot retract knowledge already shared. Pair again with a fresh code to restore authorization.

Remove from this device… is for a device that will not come back: uninstalled, reset or lost. It has its own confirmation, also affects all paired workspaces with that device, and deletes this device’s side at once: the pair records with their relay room, pair secret and sync checkpoints (the store overwrites deleted rows), the old fingerprint trust, and any session running for it (a worker in another process stops within its two-second management tick). Nothing is sent; neither the relay nor the other device is contacted. It works on an active or paused pairing and on one whose removal is pending. Paired devices highlights it when a forget is pending or the device has not answered for 7 days (since its last contact, or since pairing if it never reconnected).

The other device is not told. If it comes back, it still holds its pairing and keeps trying the paired room, where this device no longer appears, which to it looks the same as this device being offline. Each of its attempts ends with The other device hasn’t answered since 2026-10-19 14:03; it may be offline or have removed this pairing, with the time of its last contact: after 30 seconds when it joins the room (the higher device id), after 10 minutes without an answer when it opens the room. Paired devices shows it as the last connection, and imprint sync peers as last_error. After 7 days without contact its Paired devices also highlights removal: remove the pairing there the same way. The protocol cannot tell a removed pairing from an offline device without that device answering, so the message names both. Stopping an attempt on purpose (pause, forget, remove, closing Imprint) records nothing. See ADR 0073.

Pause/forget control frames run before opening either index, so revocation also works after a workspace is removed or embedding settings become incompatible. Automatic data transfer never recreates a deleted workspace implicitly.

Pair credentials live only in data/sync-pairs.sqlite3 (0600 on Unix; the data directory ACL applies on Windows) and are omitted from peer API responses, exports and snapshots. Back up this data directory as sensitive local state. Do not clone its device identity or pairing database onto another machine. The relay can correlate a stable opaque pair room but cannot read credentials, device names, workspace names or memories. Protocol-2 peers without this optional pairing extension can still perform one-time sync.

Imprint checks the pairing database’s integrity whenever it opens it and rebuilds its indexes when the check fails. Earlier Windows releases opened the file in text mode, and the C runtime then removed its last byte whenever that byte was 0x1A, which happened after about 26 automatic syncs. Sync stopped and Paired devices reported “database disk image is malformed”. The pairings themselves were intact, so the first open after updating repairs the file and automatic sync resumes. A database the rebuild cannot repair is reported with its path. Pair the device again after moving that file aside.

Sync is a deduplicating merge that also carries deletions (ADR 0176). After the manifests, each device names, in the direction data may flow, the memories the other holds that it deleted; nothing the other never held is named, so no device learns the ID of a memory it did not have. The other device deletes its copy when it is older than the deletion and passes the deletion on to its own pairings, and no device imports a memory older than a deletion it holds, so one that was away cannot bring it back. Deletions are kept 180 days.

Deleted here by Deleted on paired devices
You: the delete tool, deleting a recording, imprint purge Yes, any memory; a deleted recording’s folder too, where recordings sync
A newer version of a file, URL or inline source; an emptied file; a rewritten project profile The chunks this device recorded; another device’s chunks of its own version stay
A file gone from disk (refresh) No: its memories may still matter on the other device
imprint scrub No deletion record: the redacted version supersedes it as before, and no hash of the removed secret is kept
Moves, wipes, removing a workspace No: placement rules carry moves

One sync deletes at most half of a workspace (and at most 200 memories in a small one); a larger request is kept, not applied, and the paired device line says so. The dashboard’s paired devices list what the last sync deleted. The digest check scans local exported records, and imports can cause one extra deduplicating pass. See ADR 0020.

imprint sync serve prints one argument for the other machine: 7f3a9c21-Ab3xY9Kq on the default relay, or wss://sync.example.com/7f3a9c21-Ab3xY9Kq on any other. Before the dash is the room ID, which tells the relay which two connections to pair; after it is the 8-character PIN, generated per session, which only the two machines use. The joining side also accepts <host>/<code> and ws:// addresses. A code works for one session; serve again for a new one.

Payload framing: inside the sealed channel, data is newline-delimited JSON — {"kind":"meta",...}, {"kind":"batch","dataset":"memories"|"facts","seq":N,"records":[...]}, {"kind":"done"}, or {"kind":"error","message":"..."} when the sender cannot export. Batches hold 500 records; a frame may be up to 64 MiB.

Both machines turn the PIN in the share code into a shared key with SPAKE2 (a password-authenticated key exchange; spake2 0.9, the release magic-wormhole uses) over Ed25519, with the protocol label and room ID bound into the key:

  • The PIN never crosses the relay. The two exchange messages reveal nothing a relay can test PIN guesses against offline.
  • A relay that tries to take part gets one guess per session: a wrong PIN ends the session on the serving side.
  • HKDF-SHA256 derives a key per direction; every later frame is AES-256-GCM with the frame count as nonce, so a frame that is altered, dropped, replayed or reordered fails to open and the session stops. Nothing from a failed stream is stored.
  • A wrong PIN shows as Wrong PIN from peer on the serving side and Handshake rejected: wrong PIN on the joining side.

What the relay can see: the room ID, the two key-exchange messages, the number, sizes and timing of ciphertext frames, and the machines’ IP addresses. Not the PIN, hostnames, users, fingerprints, memories, vectors or facts. A relay can always refuse to forward (deny service), and before the session is sealed it can send a plaintext notice that ends the handshake; notices are shown as fixed text, never as what the frame says. Text from the peer (device names, error messages) is stripped of control characters before it is shown. Run your own relay if metadata such as transfer size and timing matters to you.

Compatibility: this is sync protocol 2. Versions before it sent the PIN and data in clear and are rejected on both sides — there is no unencrypted fallback a relay could force. Those versions took the PIN as a separate --pin, so given a share code they stop with –pin is required before connecting: an older joining machine cannot leak the PIN to the relay. A newer joining machine tells you when the serving one is older; an older joining machine that does connect gets update Imprint on both machines: live sync is now end-to-end encrypted (status 426). Update both machines before syncing.

Each machine states what it is willing to do. The dashboard and API call this policy direction: both, send or receive.

Serving (imprint sync serve) Dashboard Served to the peer
(none) Two-way the peer may pull and push
--send-only Send only the peer may pull; pushes are refused
--receive-only Receive only the peer may push; pulls are refused
Joining (imprint sync <code>) Dashboard This machine
(none) Receive only pulls; sends nothing
--two-way Two-way pulls, then pushes after you confirm
--send-only Send only pushes after you confirm; imports nothing

Joining defaults to receive-only because the code is all a machine needs to join: whoever handed it to you should not get your workspace unless you choose to send it.

  • Handshake. The serving side’s sealed 200 reply body is the grant plus its device: {"pull": bool, "push": bool, "device": {hostname, user, os, fingerprint}}. pull means the serving side sends; push means it imports.
  • Plan. The joining side runs pull = grant.pull AND it may receive and push = grant.push AND it may send, prints the plan (Plan: pull only — push skipped (this machine is receive-only)) and skips the other phase. The dashboard shows the same line from a {"type":"plan",...} event. If no phase remains, it fails before transferring anything: nothing to sync: pull skipped (this machine is send-only), push skipped (the provider is send-only).
  • Confirmation. When the plan includes a push, the joining side first shows the serving machine and asks: Send your workspace 'team' to studio (alice, Linux, id a3f2c1d4)? [y/N]. Anything but yes skips the push — the pull still runs, or the session ends with nothing to sync: … push skipped (you chose not to send) if there is none. The dashboard asks with Send / Don’t send buttons (a confirm_push event answered through /api/sync/approve). Trusting a device skips the serving side’s approval, never this question.
  • Enforcement. The serving side applies its own policy whatever the joining side requests: a request outside the grant gets {"status":403,"body":"provider is serving send-only: pushes are refused"} (or the receive-only equivalent) and ends the session without importing or sending anything.
  • Unknown flags are errors, so a misspelled flag cannot widen a sync. --fresh with --send-only is rejected because nothing is downloaded.

--workspace NAME syncs that workspace without changing the active one. Without it, the session uses the active workspace, captured once at start so a later imprint workspace switch or dashboard switch cannot redirect it. The workspace is passed explicitly through export and import, and both front-ends show which one is being synced.

  • Serving requires an existing workspace.
  • Joining may name a new workspace as the destination for the peer’s memories. It is registered (not switched to) when the pull starts. A join that only pushes needs an existing workspace to send from.

Every run downloads into a file of its own in the data directory (mode 0600 on Linux and macOS; on Windows the data directory’s access rules apply) and stores from it, so concurrent runs never share a download. If storing fails, the CLI keeps the download as data/sync-buffer.jsonl, and re-running the same imprint sync <code> --workspace W stores it locally without the peer, whose room has closed by then. Every line of the stream is validated (one header with vectors, batches, then done) and written re-serialised, so a malformed or hostile stream is rejected instead of kept. The buffer records its destination workspace; a run for a different workspace is refused until you finish it (--workspace <that one>) or discard it (--fresh). Buffers from versions before --workspace carry no workspace and resume into the active one. A --send-only join leaves the buffer untouched. The dashboard stores pulls from a temporary file instead.

Curate a workspace with migrate --copy, which leaves your own workspace intact, then serve only that workspace, one way:

Terminal window
# Your machine: build a team workspace from selected projects/topics
imprint migrate --from default --to team --project payments-api --copy
imprint migrate --from default --to team --topic auth --copy
imprint sync serve --workspace team --send-only
# Workspace: team
# Mode: send-only — peers can pull, pushes are refused
# imprint sync 7f3a9c21-Ab3xY9Kq
# Colleague's machine: receive it into a workspace of their own
imprint sync 7f3a9c21-Ab3xY9Kq --workspace from-alice
# Plan: pull only — push skipped (this machine is receive-only)

The colleague can read team but cannot write to it, and nothing outside team leaves your machine. Their join receives only, so nothing of theirs comes back either. Re-run the copies and the sync to refresh: memories and facts are deduplicated on both steps. Neither step deletes: a memory you remove or rewrite on your side stays in team and on the colleague’s machine until it is removed there too. In the dashboard, pick the workspace and Send only on Sync → Live Sync → Serve; the colleague pastes the code on Receive with a workspace name of their own.

Form Example Meaning
(omit --relay) imprint sync serve Use default wss://sync.alexandruleca.com
Bare host --relay sync.example.com Auto-pick scheme (wss unless localhost/127.*)
Explicit WSS --relay wss://sync.example.com Force wss://
Plain WS --relay ws://localhost:8430 Force ws:// (useful for local testing)

The serving side prints the share code with the relay address in front when it is not the default (wss://<host>/<code>); the joining side also accepts <host>/<code>. The dashboard’s Serve tab takes the same optional relay host, and Receive accepts any of these forms. With end-to-end encryption, a self-hosted relay protects transfer metadata rather than content.

The share code authenticates the session. On top of it, the serving machine decides which devices may sync, and the joining machine sees the serving device before it sends anything (confirmation):

  • Device fingerprint — stable 8-char hex ID persisted at data/device_id.txt, one per machine. It arrives inside the sealed HELLO. The serving machine asks you to accept an unknown fingerprint; fingerprints in data/trusted_devices.json are accepted without asking. A fingerprint is an identifier, not a secret — trust skips the question, never the code.

Prompt options on the serving side:

  • y / yes — accept this session only
  • t / trust — accept and remember the fingerprint
  • anything else — reject

The dashboard surfaces the same gate: after the key exchange, the server emits an approval_required event with the peer’s hostname / user / os / fingerprint, and waits. The UI renders Accept once / Trust & accept / Reject buttons that POST /api/sync/approve with {session_id, decision}; any other decision rejects. Previously-trusted fingerprints auto-accept (emitting auto_accepted instead). The trust list is shared between CLI and dashboard sessions.

Terminal window
# Use the default public relay
imprint sync serve
# → prints: imprint sync 7f3a9c21-Ab3xY9Kq
# → also prints: This device: hostname (id: a3f2c1d4)
imprint sync 7f3a9c21-Ab3xY9Kq # Machine B, default relay: receives only
imprint sync 7f3a9c21-Ab3xY9Kq --two-way # or receive, then send after confirming
# Or self-host the relay
imprint relay --port 8430
imprint sync serve --relay sync.yourdomain.com
# → prints: imprint sync wss://sync.yourdomain.com/7f3a9c21-Ab3xY9Kq
imprint sync wss://sync.yourdomain.com/7f3a9c21-Ab3xY9Kq --two-way
# → Machine A sees hostname/user/os/fingerprint + prompts [y/n/t]
# → Machine B sees Machine A's device and is asked before sending [y/N]
# → on both yes: two-way merge of the active workspace, done

Prebuilt relay images are on GHCR — see the Docker deployment guide for pinned tags, docker compose, and TLS termination.

Terminal window
imprint ui

Opens the Imprint dashboard (FastAPI + Next.js) with memory browsing, search, stats, and chat.

Vector transfers require the same encoding identity (ADR 0010). New stream records carry embedding_identity; unknown or mismatched precomputed vectors fail before upsert. Snapshot manifests also carry identity and are checked before recovery. Legacy streams/bundles require reindexing/export with matching settings. A release that changes the default embedding model changes this identity (ADR 0086), so paired devices must run the same release: vectors from a device on an earlier release are refused. Workspace migration retains vectors and therefore requires compatible collections; it is not a model conversion. Raw text/source reingestion is the rebuild path. Identity is a compatibility check, not proof of peer trust.

Precomputed imports preserve a supplied logical ID only when it matches the content/project/source identity, with or without the supplied chunk position. This keeps a manually stored note distinct from an indexed chunk at position zero and prevents sync from duplicating a note under a recalculated chunk ID. Conflicting IDs fail before upsert.

A receiving device redacts every memory and fact it imports with its own rules and settings, whatever the sending device did, except that its Privacy Filter trusts a sender’s mark (below) (secret redaction, ADR 0060). This also covers peers on releases from before redaction existed. The sender’s logical ID is validated against the content as sent, as above. A record that redaction changes is then stored under the ID of its redacted content, and embedded again from the redacted text instead of keeping the sender’s vector. Records it leaves unchanged keep their ID and vector.

When the sending device has redact.privacy_filter on and its filter runs on a GPU, it runs its redaction, rules and Privacy Filter, over the memories it sends, in full and paired syncs alike, and marks each one the redaction leaves unchanged with the filter it applied: the model’s revision and file. A receiver whose filter is on and is that same filter skips the model for a marked memory; it still applies its rules and keeps the most redacted version, as below. An unmarked memory, or one marked with another filter, is filtered on arrival as before. A memory the sender’s redaction would change is sent as stored and unmarked, so its ID and vector stay those of its stored content and the receiver redacts, relabels and embeds it again. A device whose filter runs on the CPU marks nothing: the faster side does the work (ADR 0122). The mark is trusted only because it arrives from a paired device of the same owner over the authenticated, encrypted sync channel. A device on an older release ignores the mark and filters everything; an older sender sends none.

Redaction placeholders record the length of the text they replaced, so a device can tell that a received memory is a further-redacted version of one it holds, at the same project, source and chunk position. Every write keeps the most redacted version:

  • A further-redacted version arrives. It replaces the held copy.
  • A less redacted copy arrives, from a peer that has not scrubbed or that redacts less. It is recognised and not stored.

Nothing besides the memories themselves travels, and nothing derived from a secret is kept. A scrub on one device therefore reaches every device that syncs with it, directly or through another device, and the strictest redaction any device applied wins. Facts follow the same rule, matched by their creation time.

imprint sync import <bundle> restores storage byte for byte, then scrubs the restored workspace.