Skip to content

bug(remote): bound outgoing queues when paired clients stop draining #804

Description

@Juliusolsson05

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.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions