diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.test.ts b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.ts index 25bc044226..e645d29209 100644 --- a/apps/desktop/src/app/contrib/hooks/use-background-sync.test.ts +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.ts @@ -58,6 +58,7 @@ function makeRefresh(resolveSession: ActiveTranscriptRefreshDeps['resolveSession const signatureRef = { current: new Map() } const state = createClientSessionState(ACTIVE_STORED_ID) const states = new Map([[ACTIVE_RUNTIME_ID, state]]) + const updateSessionStateRef = { updateSessionState: vi.fn((sessionId: string, updater: (value: typeof state) => typeof state) => { const next = updater(states.get(sessionId) ?? createClientSessionState(ACTIVE_STORED_ID)) @@ -66,8 +67,8 @@ function makeRefresh(resolveSession: ActiveTranscriptRefreshDeps['resolveSession return next }) } - const { updateSessionState } = updateSessionStateRef + const { updateSessionState } = updateSessionStateRef const refresh = () => reconcileActiveTranscript({ @@ -97,9 +98,11 @@ function useSyncHarness({ const updateSessionState: Parameters[0]['updateSessionState'] = vi.fn( (sessionId, updater) => { const current = {} as Parameters[0] + return updater(current) } ) + useBackgroundSync({ activeConnectionId: 'local', activeGatewayProfile: 'default', @@ -115,7 +118,7 @@ function useSyncHarness({ refreshMessagingSessions: vi.fn(), refreshSessions: vi.fn(), updateSessionState, - requestGateway: vi.fn(async () => ({ sessions: [] })) as never + requestGateway: vi.fn(async () => ({ sessions: [] })) as never }) } @@ -161,12 +164,14 @@ describe('active transcript refresh', () => { it('refreshes a hidden session through its unique complete owner route', async () => { const hiddenStoredSessionId = 'hidden-bot-chat' + const ownerRoute = { connectionId: 'ssh-bot-owner', mode: 'remote' as const, profile: 'bot-route', targetProfile: 'bot-profile' } + $changeEventsAvailable.set(true) $activeSessionId.set(ACTIVE_RUNTIME_ID) $selectedStoredSessionId.set(hiddenStoredSessionId) @@ -214,6 +219,7 @@ describe('active transcript refresh', () => { return updater(current) } ) + void updateSessionState const signatureRef = { current: new Map() } @@ -259,6 +265,7 @@ describe('active transcript refresh', () => { const TILE_STORED_ID = 'stored-tile-2' const signatureRef = { current: new Map() } + // Pre-seed the signature with what the mock returns → no-change tick. const pre = { messages: [ @@ -466,12 +473,14 @@ describe('reconcileActiveTranscript', () => { it('reads and publishes only the active hidden owner when another owner coexists', async () => { const ownerAStoredSessionId = 'owner-a-chat' const ownerBStoredSessionId = 'owner-b-hidden-chat' + const ownerBRoute = { connectionId: 'owner-b', mode: 'remote' as const, profile: 'bot-route', targetProfile: 'bot-b' } + setSessions([{ id: ownerAStoredSessionId, profile: 'bot-a', source: 'desktop' } as never]) setSessionOwnerHint(ownerAStoredSessionId, { connectionId: 'owner-a', @@ -482,9 +491,7 @@ describe('reconcileActiveTranscript', () => { setSessionOwnerHint(ownerBStoredSessionId, ownerBRoute) const fixture = makeRefresh(resolveActiveTranscriptSession) fixture.selectedStoredSessionIdRef.current = ownerBStoredSessionId - vi.mocked(getLatestSessionMessages).mockResolvedValue( - transcript('owner B answer', ownerBStoredSessionId) as never - ) + vi.mocked(getLatestSessionMessages).mockResolvedValue(transcript('owner B answer', ownerBStoredSessionId) as never) await fixture.refresh() diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts index 4962473f73..12164d7e31 100644 --- a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts @@ -130,15 +130,12 @@ export async function reconcileTileTranscripts({ try { const latest = await getLatestSessionMessages(storedSessionId) - if ( - requestId !== requestSequenceRef.current || - busyRef.current || - !stillPresent - ) { + if (requestId !== requestSequenceRef.current || busyRef.current || !stillPresent) { // Tile closed or superseded mid-read — discard AND prune its // signature so the map doesn't grow one entry per ever-opened tile // for the app's lifetime (#94255 review point 3). signatureRef.current.delete(`tile:${storedSessionId}`) + continue } @@ -156,7 +153,10 @@ export async function reconcileTileTranscripts({ runtimeSessionId, state => ({ ...state, - messages: preserveLocalAssistantErrors(graftRefreshedTailOntoBackfill(messages, state.messages), state.messages) + messages: preserveLocalAssistantErrors( + graftRefreshedTailOntoBackfill(messages, state.messages), + state.messages + ) }), storedSessionId ) @@ -199,6 +199,7 @@ export async function reconcileActiveTranscript({ profile: stored.ownerRoute.targetProfile ?? stored.ownerRoute.profile } : stored.profile + const latest = await getLatestSessionMessages(storedSessionId, profileScope) if ( @@ -219,6 +220,7 @@ export async function reconcileActiveTranscript({ storedSessionId ]) : `${stored.profile ?? 'default'}:${storedSessionId}` + const signature = sessionMessagesSignature(latest.messages) if (signatureRef.current.get(signatureKey) === signature) { @@ -656,7 +658,11 @@ export function useBackgroundSync({ // (#93942 scenario A). Signature-gated per tile, so no-change ticks // cost nothing. void reconcileTileTranscripts({ - busyRef: { get current() { return $busy.get() } }, + busyRef: { + get current() { + return $busy.get() + } + }, requestSequenceRef: tileRequestSequenceRef, signatureRef: tileSignatureRef, updateSessionState @@ -683,7 +689,14 @@ export function useBackgroundSync({ window.clearTimeout(timer) } } - }, [changeEventsAvailable, gatewayState, refreshMessagingSessions, refreshSessions, requestActiveTranscriptRefresh, updateSessionState]) + }, [ + changeEventsAvailable, + gatewayState, + refreshMessagingSessions, + refreshSessions, + requestActiveTranscriptRefresh, + updateSessionState + ]) // Keep the cron-jobs section live without a user action (scheduler ticks in // the background). cron.changed (jobs.json moved: CRUD or a scheduler tick's