A remote coding-agent interface can show a green connection indicator and still be wrong.
The socket may be alive while the UI is missing messages. A permission card may still look pending after the tool has run. A phone may reconnect and replay an old assistant chunk over a newer terminal state. These failures feel like networking bugs, but most of them are really data-consistency bugs.
A Connected Socket Can Still Show an Incomplete Truth
A WebSocket answers one narrow question: can two endpoints exchange frames right now?
A remote agent client needs answers to harder questions:
- Which events exist for this session?
- Which of those events has this client already rendered?
- Which actions reached a terminal state?
- Did another client send a newer command?
- Can history be replayed without duplicating live output?
If the protocol cannot answer those questions, reconnecting the transport only reconnects the client to uncertainty.
The Four Gaps Hidden Behind “Reconnect”
1. The transport gap
The client disconnects between event N and event N+1. This is the obvious case. The server must retain enough information for the client to resume from a known cursor rather than simply subscribing to whatever happens next.
2. The persistence gap
The relay receives an event, forwards it, and crashes before durable storage finishes. One client saw the event, but a reconnecting client cannot recover it. Acknowledging delivery before persistence creates a history that depends on who happened to be online.
3. The rendering gap
The client receives an event but suspends before its state update commits. This happens frequently on mobile. From the relay’s perspective delivery succeeded. From the user’s perspective the message never existed.
4. The semantic gap
The client has all raw events but combines them incorrectly. A permission request and its approval may arrive through different paths. A tool result may appear before the text that introduced the tool call. A resumed history chunk may overwrite a longer live message with an older partial snapshot.
The fourth gap is why “we replay everything after reconnect” is not a complete design.
Give Every Session a Monotonic Event Log
The most useful abstraction is an append-oriented event stream with stable identity and ordering.
{
"session_id": "session-123",
"event_id": "evt-9f2a",
"sequence": 1842,
"kind": "tool_result",
"parent_id": "tool-call-77",
"created_at": "2026-08-27T14:04:09Z",
"payload": { "status": "completed" }
}
The exact schema is less important than the invariants:
- An event has one stable ID across live delivery and history replay.
- Ordering is monotonic within a session.
- A client can say “resume after sequence 1842.”
- Replaying the same event is harmless.
- Child events refer to stable parents rather than array positions.
Array indexes are especially dangerous. Streaming text inserts, tool cards, questions, and subagent output do not arrive as a neat immutable list. Once a client filters or groups those events, positional references drift.
Cold History and Live Events Must Share a Deduplication Key
Many systems have two implementations: an API that loads old messages and a socket that streams new ones. If those implementations assign different identities, the reconnect boundary becomes a duplication factory.
The history API might call an item message-42 while the relay calls the same content stream-chunk-8. A client cannot safely merge them, so it either displays both or guesses based on text and timestamps.
Text-based guessing fails as soon as an agent repeats itself, edits a partial response, or emits identical tool output twice. The event ID used for live delivery should be the ID persisted into history.
Terminal States Must Only Move Forward
Permission requests, questions, tool calls, scheduled jobs, and subagents all have lifecycles. Their UI state should be monotonic.
For example:
pending -> approved -> running -> completed
-> failed
pending -> denied
pending -> expired
A stale replay must never move completed back to pending. This sounds trivial, but it is easy to violate when a snapshot endpoint and a live event handler each replace the whole object.
Merge state by version or sequence. Treat terminal states as terminal. When two sources disagree, prefer the state with stronger evidence, not the payload that arrived last at the browser.
Multi-Client Control Adds a Write-Coordination Problem
Remote supervision often means more than one client: a desktop app, browser, phone, and perhaps a shared session viewer. They may all be connected to one agent process.
Now reconnection is not only about reading missed output. It is also about coordinating commands.
- Two clients may approve the same tool.
- A phone may send a follow-up while the desktop is interrupting.
- A background tab may retry a message already accepted from another device.
Every write needs an idempotency key. The relay should acknowledge the command ID, and the agent bridge should remember enough accepted IDs to reject accidental retries. “Exactly once” is usually an illusion; idempotent “at least once” handling is a more practical target.
Mobile Backgrounding Is the Normal Case
Desktop testing makes disconnection look exceptional. Mobile makes it routine.
Operating systems suspend network activity, reclaim processes, and delay timers. The user switches from Wi-Fi to cellular, opens a camera to scan a QR code, or backgrounds the app to inspect a diff in another tool.
Design the happy path around re-entry:
- Load a bounded snapshot quickly.
- Resume from the last durable cursor.
- Merge replay with locally rendered optimistic state.
- Subscribe to live events only after the replay boundary is known.
- Expose whether the client is connected, caught up, or merely reconnecting.
“Socket open” and “session synchronized” deserve separate indicators.
Observability Should Test Invariants, Not Just Uptime
A relay health check can be green while sessions are losing data. Useful telemetry includes:
- highest persisted sequence versus highest delivered sequence;
- replay events sent and deduplicated per reconnect;
- commands retried with the same idempotency key;
- permission states rejected as regressions;
- clients connected but behind the current session cursor;
- history chunks dropped because their retention window expired.
I also found scenario testing more valuable than isolated handler tests. Suspend a mobile client during a tool approval. Drop the relay after persistence but before acknowledgement. Resume while the agent is streaming a long response. Open the same session in three clients and issue conflicting actions. Those are the tests that expose the actual protocol.
The Broader Lesson
Remote coding agents combine a terminal, chat stream, workflow engine, and distributed UI. Treating them as “a WebSocket plus some messages” hides the hardest requirements.
The connection is disposable. The event identity, ordering, action lifecycle, and recovery cursor are the product.
Building PandaPaw’s remote agent path forced me to separate those concerns. The result is not that disconnects disappear. It is that a disconnect becomes a normal transition between two consistent views of the same session.
For anyone building a remote agent interface: what invariant has been hardest for you to preserve across reconnects?