From 866f4e0d4069c6e66d32387b147c058d15a5ace3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 12 Jul 2026 23:53:45 +0200 Subject: [PATCH] fix(sync): replay live deltas after refresh failures --- packages/app/src/contexts/session-context.tsx | 5 ++++- packages/app/src/runtime/host-runtime.ts | 8 ++++++++ packages/server/src/server/session.test.ts | 3 +++ packages/server/src/server/session.ts | 7 +++++++ packages/server/src/server/websocket-server.ts | 2 +- 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 7b01b73cc5..3b0e4771ea 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -602,7 +602,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider isCurrent: () => workspaceHydrationRef.current?.id === transaction.id, }); } catch (error) { - if (workspaceHydrationRef.current === transaction) workspaceHydrationRef.current = null; + if (workspaceHydrationRef.current === transaction) { + workspaceHydrationRef.current = null; + for (const delta of transaction.deltas) applyWorkspaceUpdatePayload(delta); + } throw error; } if (!snapshot || options?.isCancelled?.()) { diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 083fecb012..c303b2b582 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -2289,6 +2289,14 @@ export class HostRuntimeStore { }); } catch (error) { if (!(error instanceof AgentDirectoryRefreshSupersededError)) { + if ( + this.agentDirectoryTransactions.get(input.serverId)?.id === transaction.id && + hasMatchingSession() + ) { + for (const delta of transaction.deltas) { + this.applyAgentDirectoryDelta(input.serverId, delta); + } + } controller.markAgentDirectorySyncError(toErrorMessage(error)); } throw error; diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index 685388b133..1df9913860 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -10,6 +10,7 @@ import { assertPullRequestAutoMergeEnableReady, } from "../services/github-service.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; import { decodeFileTransferFrame, @@ -4639,6 +4640,8 @@ test("unions viewed timelines across socket sources and removes detached sources const legacySocket = {}; session.updateClientCapabilities(null, legacySocket); + expect(session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, legacySocket)).toBe(false); + expect(session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, firstSocket)).toBe(true); messages.length = 0; forward({ type: "agent_stream", diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 8cf986d4a6..f3a666fc5a 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1017,6 +1017,13 @@ export class Session { return this.clientCapabilities.has(capability); } + supportsForSource(capability: ClientCapability, source: object): boolean { + if (capability === CLIENT_CAPS.selectiveAgentTimeline) { + return this.selectiveTimelineCapabilityBySource.get(source) ?? this.supports(capability); + } + return this.supports(capability); + } + async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise { await this.workspaceGitObserver.syncObserverForWorkspace(workspace); } diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index b8b9b1d8e3..9c0647a904 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -2025,7 +2025,7 @@ export class VoiceAssistantWebSocketServer { notification, }; const message = wrapSessionMessage( - connection?.session.supports(CLIENT_CAPS.selectiveAgentTimeline) + connection?.session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, ws) ? { type: "agent_attention_required", payload: attentionPayload,