From 65019492e5c5aee18e8bacb72e6198b69231ee80 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 5 Sep 2026 14:44:48 +0300 Subject: [PATCH] fix(sync): guard child-session discovery against overlapping pulls Child discovery now pages through the whole session list, so one pull can take longer than the 15s watchdog interval. Two overlapping pulls each read the store before either commits, so both appended the same newly found child and the directory store ended up with duplicate rows. Keep one pull per directory in flight, mirroring the status-poll guard. --- packages/ui/src/sync/sync-context.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ui/src/sync/sync-context.tsx b/packages/ui/src/sync/sync-context.tsx index f27382aa..2ce170eb 100644 --- a/packages/ui/src/sync/sync-context.tsx +++ b/packages/ui/src/sync/sync-context.tsx @@ -366,6 +366,12 @@ const pendingSessionMaterializations = new Map() +// Directories with a child-session discovery pull in flight. Discovery now +// pages through the session list, so one pull can outlast the watchdog +// interval; two overlapping pulls would each snapshot the same pre-commit +// session list and append the same child twice. +const childDiscoveryDirectories = new Set() + // Deferred completion polls awaiting their delay, keyed by directory+session so // a burst of completing messages schedules one check. const pendingMessageCompletionPolls = new Map>() @@ -2513,6 +2519,8 @@ export function SyncProvider(props: { parentSessionIds: string[], ) => { if (parentSessionIds.length === 0) return + if (childDiscoveryDirectories.has(directory)) return + childDiscoveryDirectories.add(directory) try { // Paginated so directories with > pageSize sessions are fully // discovered; a single 200-record page silently truncated the list and @@ -2557,6 +2565,8 @@ export function SyncProvider(props: { } } catch { // Best-effort — next tick will retry. + } finally { + childDiscoveryDirectories.delete(directory) } }