From 406f47629753890e1adffc6aae697e515e22e525 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 74e6d3366..7f0dc9c25 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -593,7 +593,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 083fecb01..c303b2b58 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 5ddf369fc..dea58c77a 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -6,6 +6,7 @@ import pino from "pino"; import { afterEach, describe, expect, test, vi } from "vitest"; 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, @@ -4247,6 +4248,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 41cc5a7d2..114b6e4f1 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -976,6 +976,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 426f5f4f0..d9c28e61d 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -2010,7 +2010,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,