Confirmed bounded experiment at 5d641845 (Node v24.14.1, macOS arm64, installed ws). Priority: medium; relevant when Remote is enabled and a paired phone stops draining output. Remote is off by default. This is not a claim about an observed production OOM.
Expected and actual behavior
A slow/disconnected-in-practice remote consumer must have a byte-bounded send queue. It must not retain an unlimited backlog of live agent output in the Electron main process.
RemoteServer.broadcast and send check only WebSocket readyState before send. There is no buffered-byte threshold, coalescing/overflow policy, or liveness deadline on these paths. OPEN does not imply the consumer is draining. Screen, semantic, and committed-entry events keep entering the same queue.
Reproduction and measurements
An isolated harness transpiled the unchanged RemoteServer class in memory, stubbing unused app-history imports to avoid initializing Electron/performance singletons. It connected a real ws client/server pair on an ephemeral loopback port, registered only the synthetic server socket in RemoteServer, then paused the client via client.pause().
Feed the actual broadcast method 512 synthetic screen frames with a 64 KiB recent field, yielding to the event loop every 16 frames. Record the real server socket's bufferedAmount; terminate only those test sockets afterwards. No real app connection, pairing secret, private output, or running session was used.
| Frames sent |
Encoded bytes/frame |
bufferedAmount |
Server socket OPEN |
| 128 |
65,627 |
7,810,803 |
yes |
| 256 |
65,627 |
16,212,339 |
yes |
| 512 |
65,627 |
33,015,411 (~31.5 MiB) |
yes |
The experiment intentionally stopped at 512 frames; it demonstrates continuing queue growth, not a crash. Frame size is a synthetic stress fixture, not a measured typical frame. bufferedAmount is queued transport bytes, not a heap/RSS measurement.
Impact, tradeoffs, and acceptance
A paired phone on a stalled network can increase main-process memory pressure while agents continue working. More consumers can add separate queues; no live incidence or memory growth rate is inferred here.
- Add a per-socket byte budget used by both broadcast and direct replies/bootstrap.
- Coalesce replaceable screen/process snapshots. Preserve ordering for committed/semantic events; do not silently drop transcript deltas and leave an apparently complete feed.
- Choose an explicit overflow/resync behavior (e.g. close the lagging connection and reconnect/backfill), with bounded heartbeat/liveness handling where needed.
- Verify reconnect restores durable history and current state; current reconnect alone should not be assumed to backfill every missed live event for a session already marked historyLoaded.
- Test a paused consumer alongside a draining consumer: queue bytes stay bounded, the healthy client continues receiving ordered data, and the slow client recovers without blocking desktop sessions.
Tradeoff: disconnect/resync costs a reconnect and history request; snapshot coalescing alone cannot solve an unbounded semantic/JSONL backlog. Screen deduplication from #746/#765 reduces traffic but does not establish a memory bound.
Refs #103, #365, #420. Separate from desktop IPC screen-interest work in #762 and the external operator toolkit in #795.
Related client-side retention issue: #805. Bounding socket queues does not bound the phone transcript store.
Confirmed bounded experiment at
5d641845(Node v24.14.1, macOS arm64, installedws). Priority: medium; relevant when Remote is enabled and a paired phone stops draining output. Remote is off by default. This is not a claim about an observed production OOM.Expected and actual behavior
A slow/disconnected-in-practice remote consumer must have a byte-bounded send queue. It must not retain an unlimited backlog of live agent output in the Electron main process.
RemoteServer.broadcast and send check only WebSocket readyState before
send. There is no buffered-byte threshold, coalescing/overflow policy, or liveness deadline on these paths. OPEN does not imply the consumer is draining. Screen, semantic, and committed-entry events keep entering the same queue.Reproduction and measurements
An isolated harness transpiled the unchanged RemoteServer class in memory, stubbing unused app-history imports to avoid initializing Electron/performance singletons. It connected a real
wsclient/server pair on an ephemeral loopback port, registered only the synthetic server socket in RemoteServer, then paused the client viaclient.pause().Feed the actual
broadcastmethod 512 synthetic screen frames with a 64 KiBrecentfield, yielding to the event loop every 16 frames. Record the real server socket'sbufferedAmount; terminate only those test sockets afterwards. No real app connection, pairing secret, private output, or running session was used.The experiment intentionally stopped at 512 frames; it demonstrates continuing queue growth, not a crash. Frame size is a synthetic stress fixture, not a measured typical frame.
bufferedAmountis queued transport bytes, not a heap/RSS measurement.Impact, tradeoffs, and acceptance
A paired phone on a stalled network can increase main-process memory pressure while agents continue working. More consumers can add separate queues; no live incidence or memory growth rate is inferred here.
Tradeoff: disconnect/resync costs a reconnect and history request; snapshot coalescing alone cannot solve an unbounded semantic/JSONL backlog. Screen deduplication from #746/#765 reduces traffic but does not establish a memory bound.
Refs #103, #365, #420. Separate from desktop IPC screen-interest work in #762 and the external operator toolkit in #795.
Related client-side retention issue: #805. Bounding socket queues does not bound the phone transcript store.