diff --git a/.agents/notes/proposed/bug-fix/2026-10-22-desktop-network-amplification.md b/.agents/notes/proposed/bug-fix/2026-10-22-desktop-network-amplification.md
new file mode 100644
index 0000000000..e7549a5c61
--- /dev/null
+++ b/.agents/notes/proposed/bug-fix/2026-10-22-desktop-network-amplification.md
@@ -0,0 +1,88 @@
+# Desktop network amplification: snapshot traffic and reconnect boundaries
+
+Status: proposed
+Translation: current
+
+[中文](2026-10-22-desktop-network-amplification.zh.md)
+
+## Abstract
+
+Source analysis identifies snapshot-transfer amplification and an idle recovery loop that can repeatedly download full snapshots without retry delays when a missing dependency lies beyond the bootstrap page. These mechanisms reproduce with the exact locked packages but do not establish the cause of a reported overnight traffic total. Ordinary network reconnects retain their cursor, and idle prefetch does not retry itself without another event. Lody also prevents automatic app suspension by default, independently of running tasks, so screen-off does not imply synchronization stopped. Attribute actual request/response bytes before choosing a fix; no product behavior changes in this investigation.
+
+## Scope and evidence
+
+Inspected Lody `57bb469c2a31d43b4e9c830017504bc498ee2b5e`, `loro-repo` 0.23.3, `streams-crdt` 0.19.1, `streams-client` 0.12.1 and real Loro 1.16.3. Downloaded npm tarball SHA-512 values match this checkout's lockfile. Experiments use synthetic content, in-memory HTTP boundaries and explicit completion signals; none measures a user's network or the deployed service. The installed app's revision, upload/download split and traffic counter scope remain unknown.
+
+## Full snapshot upload amplification
+
+[`createCliStreamsTransport`](../../../../apps/cli/src/lib/loro/streams-transport.ts) enables snapshot uploads with a five-second debounce. Plaintext foreground renderer transport also permits uploads. The locked SDK's `transport-snapshot.ts` and `transport-internal.ts` compare the current stream tail offset with the remote snapshot offset; the default minimum difference is only 100 KiB. Passing that gate exports the entire document snapshot, compresses it, and PUTs it. This is an absolute tail threshold, independent of the existing snapshot's size.
+
+The timer is scheduled after a successful local append. Remote imports alone do not schedule it. Repeated appends reset the debounce; unchanged offsets and a tail below the threshold do not upload. The manager rechecks the remote snapshot offset and remote-writer observations can suppress uploads. It is therefore incorrect to describe this as an unconditional upload every five seconds.
+
+A deterministic experiment invokes the exact copied SDK manager with real Loro and its bundled zstd, replacing only HEAD/PUT and the debounce wait. Eight synthetic 128 KiB writes produce 1,049,574 bytes of framed increments and eight full compressed snapshot bodies totaling 115,431,823 bytes. The initial snapshot is 16,777,540 bytes raw and 13,938,153 bytes compressed. Zero tail, a tail below 100 KiB and an unchanged post-upload cursor each produce no upload. These are measured fixture encodings and body sizes, not measured wire traffic or a production upload rate.
+
+A separate manual-clock experiment verifies debounce reset, no periodic idle upload, suppression after a changed remote version and preservation after an equal-version echo. The threshold counts all stream-tail bytes since the remote snapshot, even though only a successful local append schedules the timer.
+
+For scale only: 100 MB of successful full-snapshot transfers every 36 seconds totals 80 GB in eight hours. Real sizing must use compressed request/response bytes, not document memory or raw snapshot size.
+
+## Disposable cloud prefetch repeats bootstrap
+
+[`runEagerSyncWorkerTask`](../../../../packages/components/src/providers/eager-sync-worker-task.ts) imports its own cached snapshot, then constructs a fresh cloud transport without a matching remote cursor. The SDK creates an empty cursor store and bootstraps again; the cached document's version does not make this bootstrap request incremental. A synthetic real-adapter experiment starts with 50,000 characters and adds one character for each of two subsequent rounds. All three rounds request `/bootstrap` and receive full snapshots, even though the later replicas already hold the preceding state.
+
+This applies to the cloud worker path. Dual-mode desktop routes its own-machine prefetch through the local transport; remote-machine sessions use cloud, while cloud-only desktop uses cloud for all sessions. Automatic candidates are limited to 20, execution is serial, and the coordinator skips joined/materialized sessions and covered activity checkpoints. Sidebar/visibility/offline gates pause work. New activity or another eligible scheduling event is required; the 15-second freshness value is not an unconditional polling period.
+
+There is an additional large-document failure path. [`writeEagerSyncSnapshot`](../../../../packages/components/src/providers/eager-sync-snapshot-cache.ts) refuses a raw Loro snapshot larger than 128 MiB. The worker then returns `skipped`, and [`background-sync-coordinator.ts`](../../../../packages/components/src/providers/background-sync-coordinator.ts) records a completed activity checkpoint only for `synced`. Any session metadata event reseeds the bounded candidate list, so another session's metadata changes can requeue the same oversized document even when its own activity timestamp is unchanged. A deterministic coordinator experiment keeps both session timestamps fixed, returns `skipped`, and observes three prefetches of the large session after startup and two other-session metadata events. This is not a single integrated oversized-cloud download test; it combines a cache rejection proven in source, a real-adapter repeated-bootstrap test, and a coordinator scheduling test. The 128 MiB cache limit is raw document encoding, not compressed network size.
+
+The [serial worker decision](../../implemented/architecture/2026-09-12-session-prefetch-worker.md) deliberately avoids sharing the foreground cursor, and the [bounded window](../../implemented/architecture/2026-09-16-bounded-session-prefetch-window.md) limits scope. Any optimization must persist a worker's own snapshot and corresponding cursor together; copying another replica's cursor can skip missing data.
+
+## Connection errors and traffic are different evidence
+
+Chromium defines `ERR_CONNECTION_CLOSED` as a TCP FIN and `ERR_PROXY_CONNECTION_FAILED` as failure to resolve or connect to the proxy itself, explicitly excluding an HTTP CONNECT-stage failure. See [Chromium's error definitions](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h). A group of affected API, presence and control streams supports investigating a shared proxy path; it does not identify who closed the earlier connections or prove repeated large transfers.
+
+In the locked SDK, same-room `rejoin()` retains its cursor and ordinary live-read failures back off. A synthetic test bootstraps at offset 100000, saves offset 100123 in an injected in-memory cursor store, then injects a network failure. The next SSE request uses offset 100123 and the bootstrap count remains one; disk durability is not tested. Missing cursors, server 410 responses and incomplete-import recovery are separate bootstrap paths. The historical [read-loop spin](2026-10-19-streams-read-loop-spin.md) can stop making requests and must not be used as evidence of high network volume.
+
+Renderer Chromium networking and daemon Node networking also have different proxy owners. [`system-proxy-env.ts`](../../../../apps/electron/src/main/services/system-proxy-env.ts) fills absent child-process proxy variables from Chromium's resolved system proxy at launch. Existing environment values win. This child environment is a startup snapshot, while the renderer uses Chromium's proxy configuration; disabling the environment fallback does not disable the renderer proxy.
+
+## Idle and sleep boundaries
+
+[`CliService`](../../../../apps/electron/src/main/services/cli-service.ts) defaults `preventSleepEnabled` to true, applies a persisted override if present, and starts `prevent-app-suspension` without checking whether a task or agent is running. [Electron documents](https://www.electronjs.org/docs/latest/api/power-save-blocker) that this keeps the system active while allowing the display to turn off. [`desktop-log.ts`](../../../../apps/electron/src/main/desktop-log.ts) records suspend/resume but does not pause Streams or refresh proxies. Renderer recovery uses visibility/online events and a 60-second health check; these do not establish that the remote service or selected proxy is reachable.
+
+The prefetch coordinator has no autonomous retry after `skipped`. A manual-clock experiment finds zero remaining timers after that outcome and no retry during unchanged idle conditions; hidden-to-visible resume reseeds once. A real Repo/Flock experiment also confirms that a same-value metadata write emits no event, while a real PR or read-receipt change on another session can requeue the skipped document. Presence heartbeats use a separate ephemeral channel; unchanged PR polls do not write metadata. Therefore no-task operation weakens the repeated-upload hypothesis, and ordinary idle polling alone is not evidence of repeated full prefetch.
+
+## Idle incomplete-import recovery can bypass backoff
+
+In exact `streams-crdt` 0.19.1, `recoverLiveIncompleteRemoteApply` calls `bootstrapState`. If bootstrap is partial, that method calls `catchupOrGone`; when catch-up reaches the tail with missing CRDT dependencies, it calls `bootstrapState` again. The retry remains inside the recovery call and never reaches its outer backoff. The SSE inactivity timer is cleared on entry to the error handler, before recovery. Successful individual requests do not impose a bound on the entire recursive recovery.
+
+A bounded experiment runs the real SDK read loop, real Loro adapter and bundled zstd with an injected already-joined read-only state and synthetic client responses. The host intentionally omits one causal predecessor and places the dependent update beyond the first bootstrap page. With zero local appends it returns three identical 3,485,325-byte compressed snapshots (10,455,975 bytes total), zero cursor saves and zero backoff timers before the experiment stops the fourth attempt. The control places that same incomplete tail in a complete bootstrap response: one snapshot is read, then backoff is scheduled. This tests the SDK recovery algorithm, not real HTTP, desktop sleep or production stream corruption.
+
+Partial bootstrap is a supported server behavior, not an invented response shape. The inspected [server at `470ce12`](https://github.com/loro-dev/loro-streams/blob/470ce12d9c7e734e2c42cef11a3e93438c4b0c22/rr/src/protocol.rs#L4187) bounds the retained update page, defaults to a 64 MiB tail budget, and streams the snapshot separately. The [protocol](https://github.com/loro-dev/loro-streams/blob/470ce12d9c7e734e2c42cef11a3e93438c4b0c22/specs/ds-extensions.md#L377) requires ordinary GET continuation after a partial bootstrap. The experiment uses a smaller synthetic page; under the inspected default the bad tail must lie beyond the much larger initial page. The deployed server revision/configuration and the user's missing dependencies remain unverified.
+
+A second, separate gap accepts a regressing response offset when the current cursor still equals the request offset (`resolveNextOffsetAfterConcurrentAdvance`). A real-SDK synthetic SSE experiment alternates an empty control returning offset zero with existing updates: six reads replay 786,729 framed bytes without local writes or document growth. This requires the remote response to return an older offset; there is no evidence that the user's server or proxy did so. It is not a consequence of sleep alone.
+
+## Proposed containment and available logs
+
+### Persist imported pending updates as a separate payload
+
+Further source analysis and real Loro 1.16.3 experiments confirm a separate persistence gap: ordinary snapshot and full-history update exports omit remotely imported updates parked for missing causal dependencies. With an existing `base|` document, receiving B before its predecessor A leaves B pending. Without restarting, supplying A produces `base|AB`. Restoring only the ordinary snapshot and then supplying A produces `base|A`; restoring the snapshot plus saved original B bytes and then supplying A produces `base|AB`. This concerns imported remote pending changes, not an uncommitted local transaction.
+
+The separate [Loro implementation](https://github.com/loro-dev/loro/pull/1220) has been rebased onto core main `136b944f2501b16eb82ef1c51541627958199d75`, including the published Rust 1.16.2 fixes, and now provides `exportPendingUpdates(): Uint8Array | undefined` and Rust `export_pending_updates() -> Result