From b4b640b36b056e2f4ce8a34ce455652c106d3f33 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 18 Jul 2026 15:47:30 +0700 Subject: [PATCH] fix(flows): reconcile the cache after a websocket reconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GraphQL subscriptions are delta-only: the server registers a subscriber for future events and never replays what it published while a client was disconnected (and it drops events for a disconnected subscriber outright). Nothing on the client refetched after the socket came back, so a websocket drop during an active flow — a network blip, a laptop waking, a proxy timeout — left a permanent hole in the streamed logs/messages/status until the user manually reloaded the page. Refetch the active queries on reconnect: graphql-ws hands `wasRetry` to the `connected` handler, so on a retry (not the initial connect) call `refetchObservableQueries()`, which re-runs the flow's queries and merges the full current state back into the cache. Verified live with a faithful drop (patched WebSocket, real `ws.close()` mid- flow while the agent kept producing): before the fix the messages created during the outage stayed missing after reconnect and only a reload recovered them; with the fix they reappear automatically on reconnect, no reload. (A CDP "offline" emulation does NOT reproduce this — it buffers the socket rather than closing it, so use a real close when testing.) 1006 tests pass. Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/apollo.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/apollo.ts b/frontend/src/lib/apollo.ts index c4fabff7..c2015bc2 100644 --- a/frontend/src/lib/apollo.ts +++ b/frontend/src/lib/apollo.ts @@ -383,6 +383,11 @@ const replaceWithIncoming = { }; const createApolloClient = () => { + // Holds the client for the ws `connected` handler, which is defined before the + // client exists; `lazy: true` means the socket only opens on the first + // subscription, after `.current` is set below. + const clientRef: { current?: ApolloClient } = {}; + const httpLink = new HttpLink({ credentials: 'include', uri: `${window.location.origin}${GRAPHQL_ENDPOINT}`, @@ -393,7 +398,16 @@ const createApolloClient = () => { lazy: true, on: { closed: () => Log.debug('GraphQL WebSocket closed'), - connected: () => Log.debug('GraphQL WebSocket connected'), + connected: (_socket, _payload, wasRetry) => { + Log.debug('GraphQL WebSocket connected'); + + // Subscriptions are delta-only — the server never replays events + // published while we were disconnected — so on a reconnect refetch + // active queries to reconcile the cache with the backend. + if (wasRetry) { + void clientRef.current?.refetchObservableQueries(); + } + }, connecting: () => Log.debug('GraphQL WebSocket connecting...'), error: (error) => { Log.error('GraphQL WebSocket error:', error); @@ -517,7 +531,7 @@ const createApolloClient = () => { const link = ApolloLink.from([errorLink, subscriptionCacheLink, streamingLink, transportLink]); - return new ApolloClient({ + const apolloClient = new ApolloClient({ cache, defaultOptions: { watchQuery: { @@ -528,6 +542,10 @@ const createApolloClient = () => { }, link, }); + + clientRef.current = apolloClient; + + return apolloClient; }; export const client = createApolloClient();