From ea5f0aba99fecbe6f81fea78b9d992ae057d5dfa Mon Sep 17 00:00:00 2001 From: Viktor Pelle Date: Fri, 24 Apr 2026 20:46:59 +0200 Subject: [PATCH] better sync checks --- .../cli/src/daemon/routes/context-graph.ts | 49 +++++++++++++++++-- .../ui/components/Modals/JoinProjectModal.tsx | 48 +++++++++--------- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/daemon/routes/context-graph.ts b/packages/cli/src/daemon/routes/context-graph.ts index 3b6e97d98..47c0fa1b1 100644 --- a/packages/cli/src/daemon/routes/context-graph.ts +++ b/packages/cli/src/daemon/routes/context-graph.ts @@ -1140,6 +1140,52 @@ export async function handleContextGraphRoutes(ctx: RequestContext): Promise + | undefined; + const existingSub = subMap?.get(paranetId); + const existingJobId = catchupTracker.latestByParanet.get(paranetId); + const existingJob = existingJobId ? catchupTracker.jobs.get(existingJobId) : undefined; + + if (existingSub?.subscribed) { + if (existingJob && (existingJob.status === "queued" || existingJob.status === "running")) { + return jsonResponse(res, 200, { + subscribed: paranetId, + catchup: { + status: existingJob.status, + includeWorkspace: existingJob.includeWorkspace, + jobId: existingJob.jobId, + }, + }); + } + + if (existingSub.synced) { + const jobId = existingJob?.jobId ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + if (!existingJob) { + const syntheticJob: CatchupJob = { + jobId, + paranetId, + includeWorkspace: shouldSyncSharedMemory, + status: "done", + queuedAt: Date.now(), + startedAt: Date.now(), + finishedAt: Date.now(), + }; + catchupTracker.jobs.set(jobId, syntheticJob); + catchupTracker.latestByParanet.set(paranetId, jobId); + } + return jsonResponse(res, 200, { + subscribed: paranetId, + catchup: { + status: "done", + includeWorkspace: shouldSyncSharedMemory, + jobId, + }, + }); + } + } + console.log(`[subscribe] contextGraph=${paranetId} includeSharedMemory=${shouldSyncSharedMemory}`); agent.subscribeToContextGraph(paranetId); @@ -1208,9 +1254,6 @@ export async function handleContextGraphRoutes(ctx: RequestContext): Promise - | undefined; const sub = subMap?.get(paranetId); if (sub) { sub.synced = true; diff --git a/packages/node-ui/src/ui/components/Modals/JoinProjectModal.tsx b/packages/node-ui/src/ui/components/Modals/JoinProjectModal.tsx index 7e6ccc725..7fd73b84a 100644 --- a/packages/node-ui/src/ui/components/Modals/JoinProjectModal.tsx +++ b/packages/node-ui/src/ui/components/Modals/JoinProjectModal.tsx @@ -70,6 +70,7 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP const [progress, setProgress] = useState(''); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const [syncInProgress, setSyncInProgress] = useState(false); const [requestSent, setRequestSent] = useState(false); const [sendingRequest, setSendingRequest] = useState(false); const [accessDenied, setAccessDenied] = useState(false); @@ -93,6 +94,7 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP setInviteCode(initialContextGraphId ?? ''); setError(null); setSuccess(false); + setSyncInProgress(false); setRequestSent(false); setAccessDenied(false); setProgress(''); @@ -112,6 +114,7 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP setJoining(true); setError(null); setSuccess(false); + setSyncInProgress(false); setRequestSent(false); setAccessDenied(false); @@ -128,6 +131,7 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP setProgress('Subscribing to project…'); const subResult = await subscribeToContextGraph(cgId); + const subscribed = !!subResult?.subscribed; setProgress('Syncing knowledge from peers…'); @@ -152,26 +156,13 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP } if (catchup.status === 'timeout') { - // A poll timeout is NOT evidence of ACL denial — it just means - // no peer finished the catchup within ~90s. Common reasons: - // - project is public but peers are slow / offline, - // - network path is congested, - // - our subscribe hasn't reached a peer that holds the CG yet. - // Flipping `accessDenied` here used to push users of public - // projects straight into the "Access Restricted — send signed - // join request" flow, which is misleading and cuts them off - // from just retrying. Surface a neutral network error instead - // and let them retry; a real ACL denial lands in the `denied` - // branch above, or in the `err.message` check at the bottom - // of this function. (HEAD tier-4c G3; v10-rc's copy "syncing - // still in progress" was milder but still implied success — - // we'd rather the user retry explicitly than think the subscribe - // finished when the background sync never landed data.) - setError( - 'Timed out waiting for peers to respond. The project may be slow to catch up, or no peer currently holds the data. Try again in a moment.', - ); - setProgress(''); - return; + if (!subscribed) { + setError( + 'Timed out waiting for peers to respond. The project may be slow to catch up, or no peer currently holds the data. Try again in a moment.', + ); + setProgress(''); + return; + } } setProgress('Refreshing project list…'); @@ -184,7 +175,15 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP openTab({ id: `project:${joined.id}`, label: joined.name || joined.id, closable: true }); } + if (catchup.status === 'timeout') { + setSuccess(true); + setSyncInProgress(true); + setProgress(''); + return; + } + setSuccess(true); + setSyncInProgress(false); setProgress(''); // Phase 8: transition into wire-workspace step instead of // auto-closing. The joiner can either install workspace files @@ -291,10 +290,13 @@ export function JoinProjectModal({ open, onClose, initialContextGraphId }: JoinP {success && (
- Successfully joined! Syncing knowledge from peers… + {syncInProgress + ? 'Project joined successfully. Initial sync is still in progress and data will appear as peers respond.' + : 'Successfully joined! Syncing knowledge from peers…'}
)}