@taskforcehq/taskforce 0.3.302 → 0.3.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -20,100 +20,21 @@
20
20
  */
21
21
  // === SECTION: Imports & module-level constants ===
22
22
  import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
23
- import { ensureCloudWorkspaceReadyForSyncService, isTransientWorkspaceSyncStatus, runWorkspacePullSyncService, runWorkspacePushSyncService, syncUserGlobalSettingsService } from '../sync/syncService';
23
+ import { ensureCloudWorkspaceReadyForSyncService, syncUserGlobalSettingsService } from '../sync/syncService';
24
24
  import { buildWorkspaceSyncPayloadModel, buildWorkspaceSyncSignatureModel } from '../sync/workspaceSyncModel';
25
- import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
26
25
  import { useWorkspaceSyncController } from './useWorkspaceSyncController';
27
26
  import { normalizeTaskforceTheme } from '../utils/theme';
28
27
  import { useRealtimeSync } from './useRealtimeSync';
29
28
  import { TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT } from '../utils/contextAssetEvents';
30
29
  import { setLocale as setAppLocale } from '../localization';
31
30
  import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
32
- const USER_GLOBAL_SYNC_META_KEY = 'taskforce.userGlobalSyncMeta.v1';
33
- const SYNC_DEBUG_LOG_KEY = 'taskforce.syncDebug.v1';
34
- const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120_000;
35
- const WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX = 'taskforce.syncWatermarks.v3';
36
- const WORKSPACE_SYNC_WATERMARKS_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
37
- const WORKSPACE_CONCURRENT_WINDOW_MESSAGE = 'Another local window is already syncing this workspace. Wait a few seconds, or press Repair in that window.';
31
+ import { createSyncAuthCoordinator } from './sync/auth';
32
+ import { createBootstrapTimeoutMonitor, syncBootstrapPhaseTracking } from './sync/bootstrap';
33
+ import { buildTaxonomyStateFingerprint, flattenDocumentReviewComments, flattenTaskEvents, isValidWorkspaceKey, logSyncDebug, normalizeWorkspaceSyncAiProfileSnapshotEntry, readPersistedSyncWatermarks, readUserGlobalSyncMeta, WORKSPACE_CONCURRENT_WINDOW_MESSAGE, WORKSPACE_FULL_RECONCILE_INTERVAL_MS, writeUserGlobalSyncMeta } from './sync/orchestratorShared';
34
+ import { createRealtimeSyncBridge } from './sync/realtime';
35
+ import { createSyncRecoveryCoordinator } from './sync/recovery';
36
+ import { createWorkspaceTransferCoordinator } from './sync/transfers';
38
37
  // === SECTION: Pure utility functions ===
39
- function isValidWorkspaceKey(workspaceId) {
40
- const workspaceKey = String(workspaceId || '').trim();
41
- return workspaceKey.length > 0 && workspaceKey.toLowerCase() !== 'default';
42
- }
43
- function isSyncDebugLoggingEnabled() {
44
- if (typeof window === 'undefined')
45
- return false;
46
- try {
47
- const raw = String(window.localStorage.getItem(SYNC_DEBUG_LOG_KEY) || '').trim().toLowerCase();
48
- return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
49
- }
50
- catch {
51
- return false;
52
- }
53
- }
54
- function logSyncDebug(event, details) {
55
- if (!isSyncDebugLoggingEnabled())
56
- return;
57
- const payload = details && typeof details === 'object' ? details : {};
58
- console.info('[Taskforce Sync]', event, payload);
59
- }
60
- export function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry) {
61
- const metadata = entry?.providerMetadata ?? entry?.provider_metadata ?? null;
62
- return {
63
- id: String(entry?.id || '').trim(),
64
- workspaceId: String(entry?.workspaceId || entry?.workspace_id || '').trim(),
65
- profileToken: String(entry?.profileToken || entry?.profile_token || '').trim(),
66
- name: String(entry?.name || '').trim(),
67
- username: String(entry?.username || '').trim(),
68
- icon: String(entry?.icon || 'Bot').trim() || 'Bot',
69
- color: String(entry?.color || '#8b5cf6').trim() || '#8b5cf6',
70
- surfaceType: parseAiProfileSurfaceType(entry?.surfaceType ?? entry?.surface_type),
71
- providerMetadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata)
72
- ? metadata
73
- : null,
74
- createdAt: String(entry?.createdAt || entry?.created_at || '').trim(),
75
- updatedAt: String(entry?.updatedAt || entry?.updated_at || entry?.createdAt || entry?.created_at || '').trim(),
76
- lastActiveAt: typeof entry?.lastActiveAt === 'string' && entry.lastActiveAt.trim().length > 0
77
- ? entry.lastActiveAt.trim()
78
- : typeof entry?.last_active_at === 'string' && entry.last_active_at.trim().length > 0
79
- ? entry.last_active_at.trim()
80
- : null
81
- };
82
- }
83
- function readUserGlobalSyncMeta() {
84
- if (typeof window === 'undefined')
85
- return {};
86
- try {
87
- const raw = window.localStorage.getItem(USER_GLOBAL_SYNC_META_KEY);
88
- if (!raw)
89
- return {};
90
- const parsed = JSON.parse(raw);
91
- return parsed && typeof parsed === 'object'
92
- ? parsed
93
- : {};
94
- }
95
- catch {
96
- return {};
97
- }
98
- }
99
- function writeUserGlobalSyncMeta(next) {
100
- if (typeof window === 'undefined')
101
- return;
102
- try {
103
- window.localStorage.setItem(USER_GLOBAL_SYNC_META_KEY, JSON.stringify(next));
104
- }
105
- catch {
106
- // best-effort only
107
- }
108
- }
109
- function buildTaxonomyStateFingerprint(taxonomyState) {
110
- try {
111
- return JSON.stringify(taxonomyState && typeof taxonomyState === 'object' ? taxonomyState : null);
112
- }
113
- catch {
114
- return 'null';
115
- }
116
- }
117
38
  function summarizeSyncPhase(enabled, phase, busy, retryAt, lastErrorMessage) {
118
39
  if (!enabled)
119
40
  return 'Sync is turned off for this workspace.';
@@ -156,84 +77,6 @@ function recommendSyncAction(enabled, phase, retryAt, lastErrorMessage) {
156
77
  }
157
78
  return 'No action needed.';
158
79
  }
159
- function flattenDocumentReviewComments(sessions) {
160
- return sessions.flatMap((session) => {
161
- const sessionId = String(session?.id || '').trim();
162
- if (!sessionId || !Array.isArray(session?.comments))
163
- return [];
164
- return session.comments
165
- .map((comment) => ({
166
- id: String(comment?.id || '').trim(),
167
- sessionId,
168
- body: String(comment?.body || ''),
169
- anchor: comment?.anchor ?? null,
170
- order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment.order))) : 0,
171
- authorActorId: typeof comment?.authorActorId === 'string' && comment.authorActorId.trim().length > 0 ? comment.authorActorId.trim() : null,
172
- createdAt: String(comment?.createdAt || '').trim(),
173
- updatedAt: String(comment?.updatedAt || comment?.createdAt || '').trim(),
174
- deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
175
- }))
176
- .filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
177
- });
178
- }
179
- function readPersistedSyncWatermarks(workspaceId, taxonomyStateFingerprint) {
180
- if (typeof window === 'undefined')
181
- return null;
182
- try {
183
- const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
184
- const raw = window.localStorage.getItem(key);
185
- if (!raw)
186
- return null;
187
- const parsed = JSON.parse(raw);
188
- if (!parsed || (parsed.v !== 2 && parsed.v !== 3 && parsed.v !== 4 && parsed.v !== 5 && parsed.v !== 6 && parsed.v !== 7))
189
- return null;
190
- const savedAt = Date.parse(String(parsed.savedAt || ''));
191
- if (!Number.isFinite(savedAt) || Date.now() - savedAt > WORKSPACE_SYNC_WATERMARKS_MAX_AGE_MS)
192
- return null;
193
- const savedTaxonomyStateFingerprint = typeof parsed.taxonomyStateFingerprint === 'string'
194
- ? parsed.taxonomyStateFingerprint
195
- : '';
196
- const taskWatermarks = (!taxonomyStateFingerprint || !savedTaxonomyStateFingerprint || savedTaxonomyStateFingerprint === taxonomyStateFingerprint)
197
- ? new Map(Array.isArray(parsed.taskWatermarks) ? parsed.taskWatermarks : [])
198
- : new Map();
199
- return {
200
- taskWatermarks,
201
- initiativeWatermarks: new Map(Array.isArray(parsed.initiativeWatermarks) ? parsed.initiativeWatermarks : []),
202
- workstreamWatermarks: new Map(Array.isArray(parsed.workstreamWatermarks) ? parsed.workstreamWatermarks : []),
203
- documentWatermarks: new Map(Array.isArray(parsed.documentWatermarks) ? parsed.documentWatermarks : []),
204
- assetWatermarks: new Map(Array.isArray(parsed.assetWatermarks) ? parsed.assetWatermarks : []),
205
- documentReviewSessionWatermarks: new Map(Array.isArray(parsed.documentReviewSessionWatermarks) ? parsed.documentReviewSessionWatermarks : []),
206
- documentReviewCommentWatermarks: new Map(Array.isArray(parsed.documentReviewCommentWatermarks) ? parsed.documentReviewCommentWatermarks : []),
207
- annotatedAttachmentSessionWatermarks: new Map(Array.isArray(parsed.annotatedAttachmentSessionWatermarks) ? parsed.annotatedAttachmentSessionWatermarks : []),
208
- };
209
- }
210
- catch {
211
- return null;
212
- }
213
- }
214
- function savePersistedSyncWatermarks(workspaceId, data) {
215
- if (typeof window === 'undefined')
216
- return;
217
- try {
218
- const key = `${WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX}.${workspaceId}`;
219
- window.localStorage.setItem(key, JSON.stringify({
220
- v: 7,
221
- savedAt: new Date().toISOString(),
222
- taskWatermarks: Array.from(data.taskWatermarks.entries()),
223
- initiativeWatermarks: Array.from(data.initiativeWatermarks.entries()),
224
- workstreamWatermarks: Array.from(data.workstreamWatermarks.entries()),
225
- documentWatermarks: Array.from(data.documentWatermarks.entries()),
226
- assetWatermarks: Array.from(data.assetWatermarks.entries()),
227
- documentReviewSessionWatermarks: Array.from(data.documentReviewSessionWatermarks.entries()),
228
- documentReviewCommentWatermarks: Array.from(data.documentReviewCommentWatermarks.entries()),
229
- annotatedAttachmentSessionWatermarks: Array.from(data.annotatedAttachmentSessionWatermarks.entries()),
230
- taxonomyStateFingerprint: typeof data.taxonomyStateFingerprint === 'string' ? data.taxonomyStateFingerprint : ''
231
- }));
232
- }
233
- catch {
234
- // localStorage may be full or unavailable; non-fatal
235
- }
236
- }
237
80
  const EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS = {
238
81
  documents: false,
239
82
  aiProfiles: false,
@@ -241,6 +84,7 @@ const EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS = {
241
84
  documentReviewSessions: false,
242
85
  annotatedAttachmentSessions: false
243
86
  };
87
+ export { normalizeWorkspaceSyncAiProfileSnapshotEntry };
244
88
  export function useSyncOrchestrator(input) {
245
89
  const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, authSessionResolved, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, initiatives, workstreams, taxonomies, taxonomyState, setupState, globalTheme, locale, globalWeekStartsOn, themeUseGlobalDefault, setTasks, setArchivedTasks, setAuthBlocked, setIsAuthenticated, checkAuthSession, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn, fetchPlanningEntities } = input;
246
90
  const taxonomyStateFingerprint = useMemo(() => buildTaxonomyStateFingerprint(taxonomyState), [taxonomyState]);
@@ -381,8 +225,12 @@ export function useSyncOrchestrator(input) {
381
225
  workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
382
226
  workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
383
227
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
228
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map();
384
229
  workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
385
230
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
231
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map();
232
+ workspaceStartupFullReconcileRanRef.current = false;
233
+ workspaceFullReconcileInFlightRef.current = false;
386
234
  const persisted = readPersistedSyncWatermarks(currentWorkspaceId, taxonomyStateFingerprint);
387
235
  if (persisted) {
388
236
  workspaceLastPushedWatermarksRef.current = persisted.taskWatermarks;
@@ -393,6 +241,7 @@ export function useSyncOrchestrator(input) {
393
241
  workspaceLastPushedDocumentReviewSessionWatermarksRef.current = persisted.documentReviewSessionWatermarks;
394
242
  workspaceLastPushedDocumentReviewCommentWatermarksRef.current = persisted.documentReviewCommentWatermarks;
395
243
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = persisted.annotatedAttachmentSessionWatermarks;
244
+ workspaceLastPushedTaskEventWatermarksRef.current = persisted.taskEventWatermarks;
396
245
  }
397
246
  }, [currentWorkspaceId, taxonomyStateFingerprint]);
398
247
  const markWorkspaceSyncReferenceSnapshotReady = useCallback((key) => {
@@ -404,7 +253,7 @@ export function useSyncOrchestrator(input) {
404
253
  }));
405
254
  }, []);
406
255
  const workspaceSyncReferenceSnapshotsReady = Object.values(workspaceSyncReferenceSnapshotStatus).every(Boolean);
407
- const { workspaceRetryAt, isWorkspaceRetryPending, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, scheduleWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease } = useWorkspaceSyncController({
256
+ const { workspaceRetryAt, isWorkspaceRetryPending, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, scheduleWorkspaceRetry, persistWorkspaceSyncPatch, applyWorkspaceSyncPatchLocally, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease } = useWorkspaceSyncController({
408
257
  currentWorkspaceId,
409
258
  setWorkspaceCloudSyncEnabled,
410
259
  setWorkspaceSyncPhase,
@@ -429,7 +278,9 @@ export function useSyncOrchestrator(input) {
429
278
  const workspaceLastPushedDocumentReviewCommentWatermarksRef = useRef(new Map());
430
279
  const workspaceLastPushedAnnotatedAttachmentSessionIdsRef = useRef(new Set());
431
280
  const workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef = useRef(new Map());
281
+ const workspaceLastPushedTaskEventWatermarksRef = useRef(new Map());
432
282
  const workspaceStartupRepairRanRef = useRef(false);
283
+ const workspaceStartupFullReconcileRanRef = useRef(false);
433
284
  const workspaceRepairModeRef = useRef(false);
434
285
  const workspaceAttachBootstrapBaselineSignatureRef = useRef('');
435
286
  const workspaceCaptureAttachBootstrapBaselineRef = useRef(false);
@@ -656,51 +507,44 @@ export function useSyncOrchestrator(input) {
656
507
  operation
657
508
  });
658
509
  }, [currentWorkspaceId]);
510
+ const persistWorkspaceSyncPatchBestEffort = useCallback(async (patch) => {
511
+ try {
512
+ return await persistWorkspaceSyncPatch(patch);
513
+ }
514
+ catch {
515
+ return applyWorkspaceSyncPatchLocally(patch);
516
+ }
517
+ }, [applyWorkspaceSyncPatchLocally, persistWorkspaceSyncPatch]);
659
518
  const persistWorkspaceSyncPatchSafely = useCallback((patch) => {
660
- void persistWorkspaceSyncPatch(patch).catch(() => { });
661
- }, [persistWorkspaceSyncPatch]);
519
+ void persistWorkspaceSyncPatchBestEffort(patch).catch(() => { });
520
+ }, [persistWorkspaceSyncPatchBestEffort]);
662
521
  const scheduleWorkspaceSyncRetry = useCallback((minDelayMs) => {
663
522
  scheduleWorkspaceRetry(() => retryWorkspaceSyncRef.current(), { minDelayMs });
664
523
  }, [scheduleWorkspaceRetry]);
665
524
  useEffect(() => {
666
- if (workspaceSyncPhase === 'attach-cloud' || workspaceSyncPhase === 'provision-local') {
667
- lastBootstrapPhaseRef.current = workspaceSyncPhase;
668
- if (bootstrapPhaseStartedAtRef.current === null) {
669
- bootstrapPhaseStartedAtRef.current = Date.now();
670
- }
671
- bootstrapLastProgressAtRef.current = null;
672
- return;
673
- }
674
- bootstrapPhaseStartedAtRef.current = null;
675
- bootstrapLastProgressAtRef.current = null;
525
+ syncBootstrapPhaseTracking({
526
+ workspaceSyncPhase,
527
+ lastBootstrapPhaseRef,
528
+ bootstrapPhaseStartedAtRef,
529
+ bootstrapLastProgressAtRef
530
+ });
676
531
  }, [workspaceSyncPhase]);
677
532
  useEffect(() => {
678
- if (!workspaceCloudSyncEnabled)
679
- return;
680
- if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
681
- return;
682
- const intervalId = window.setInterval(() => {
683
- if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
684
- return;
685
- if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
686
- return;
687
- const lastActivity = Math.max(bootstrapPhaseStartedAtRef.current ?? 0, bootstrapLastProgressAtRef.current ?? 0);
688
- if (lastActivity > 0 && (Date.now() - lastActivity) < WORKSPACE_BOOTSTRAP_TIMEOUT_MS)
689
- return;
690
- const phaseLabel = workspaceSyncPhase === 'attach-cloud' ? 'initial cloud pull' : 'initial cloud upload';
691
- const message = `Workspace sync ${phaseLabel} stalled. Press Repair to restart sync for this workspace.`;
692
- setWorkspaceLastErrorMessage(message);
693
- setWorkspaceLastErrorAt(new Date().toISOString());
694
- setUserGlobalSyncStatus('error');
695
- setUserGlobalSyncError(message);
696
- setWorkspaceSyncBusy(false);
697
- persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
698
- logSyncDebug('workspace_sync_bootstrap_timeout', {
699
- workspaceId: currentWorkspaceId,
700
- phase: workspaceSyncPhase
701
- });
702
- }, 10_000);
703
- return () => window.clearInterval(intervalId);
533
+ return createBootstrapTimeoutMonitor({
534
+ currentWorkspaceId,
535
+ workspaceCloudSyncEnabled,
536
+ workspaceSyncPhase,
537
+ workspacePullInFlightRef,
538
+ workspacePushInFlightRef,
539
+ bootstrapPhaseStartedAtRef,
540
+ bootstrapLastProgressAtRef,
541
+ setWorkspaceLastErrorMessage: (value) => setWorkspaceLastErrorMessage(value),
542
+ setWorkspaceLastErrorAt: (value) => setWorkspaceLastErrorAt(value),
543
+ setUserGlobalSyncStatus,
544
+ setUserGlobalSyncError: (value) => setUserGlobalSyncError(value),
545
+ setWorkspaceSyncBusy,
546
+ persistWorkspaceSyncPatchSafely
547
+ });
704
548
  }, [
705
549
  currentWorkspaceId,
706
550
  workspaceCloudSyncEnabled,
@@ -739,6 +583,7 @@ export function useSyncOrchestrator(input) {
739
583
  workspaceWorkstreamsRef.current = workstreams || [];
740
584
  }, [workstreams]);
741
585
  const workspaceForceFullAiProfilePushRef = useRef(false);
586
+ const workspaceFullReconcileInFlightRef = useRef(false);
742
587
  const buildWorkspaceSyncPayload = useCallback((options) => {
743
588
  const forceAllAiProfiles = options?.forceAllAiProfiles === true;
744
589
  // Read documents and assets from refs rather than state so that the freshest data
@@ -747,6 +592,7 @@ export function useSyncOrchestrator(input) {
747
592
  return buildWorkspaceSyncPayloadModel({
748
593
  tasks: workspaceTasksRef.current,
749
594
  archivedTasks: workspaceArchivedTasksRef.current,
595
+ taskEvents: flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current),
750
596
  lastPushedTaskIds: workspaceLastPushedTaskIdsRef.current,
751
597
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
752
598
  initiatives: workspaceInitiativesRef.current,
@@ -775,6 +621,7 @@ export function useSyncOrchestrator(input) {
775
621
  lastPushedDocumentReviewCommentWatermarks: workspaceLastPushedDocumentReviewCommentWatermarksRef.current,
776
622
  lastPushedAnnotatedAttachmentSessionIds: workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current,
777
623
  lastPushedAnnotatedAttachmentSessionWatermarks: workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current,
624
+ lastPushedTaskEventWatermarks: workspaceLastPushedTaskEventWatermarksRef.current,
778
625
  lastPushedWatermarks: workspaceLastPushedWatermarksRef.current
779
626
  });
780
627
  }, [taxonomies, taxonomyState]);
@@ -802,6 +649,7 @@ export function useSyncOrchestrator(input) {
802
649
  workspaceId: currentWorkspaceId,
803
650
  tasks: workspaceTasksRef.current,
804
651
  archivedTasks: workspaceArchivedTasksRef.current,
652
+ taskEvents: flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current),
805
653
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
806
654
  initiatives: workspaceInitiativesRef.current,
807
655
  workstreams: workspaceWorkstreamsRef.current,
@@ -813,7 +661,52 @@ export function useSyncOrchestrator(input) {
813
661
  documentReviewComments: workspaceSyncDocumentReviewCommentsRef.current,
814
662
  annotatedAttachmentSessions: workspaceSyncAnnotatedAttachmentSessionsRef.current
815
663
  });
816
- }, [currentWorkspaceId, taxonomies]);
664
+ }, [currentWorkspaceId, taxonomies, taxonomyState]);
665
+ const workspaceCoreSyncFingerprint = useMemo(() => JSON.stringify({
666
+ tasks: (tasks || [])
667
+ .map((task) => ({
668
+ id: String(task.id || '').trim(),
669
+ updatedAt: String(task.updatedAt || task.createdAt || '').trim(),
670
+ status: String(task.status || '').trim(),
671
+ referenceNumber: task.referenceNumber ?? null,
672
+ localReferenceNumber: task.localReferenceNumber ?? null
673
+ }))
674
+ .sort((left, right) => left.id.localeCompare(right.id)),
675
+ archivedTasks: (archivedTasks || [])
676
+ .map((task) => ({
677
+ id: String(task.id || '').trim(),
678
+ updatedAt: String(task.updatedAt || task.createdAt || '').trim(),
679
+ status: String(task.status || '').trim(),
680
+ referenceNumber: task.referenceNumber ?? null,
681
+ localReferenceNumber: task.localReferenceNumber ?? null
682
+ }))
683
+ .sort((left, right) => left.id.localeCompare(right.id)),
684
+ initiatives: (initiatives || [])
685
+ .map((initiative) => ({
686
+ id: String(initiative.id || '').trim(),
687
+ updatedAt: String(initiative.updatedAt || initiative.createdAt || '').trim(),
688
+ isArchived: Boolean(initiative.isArchived)
689
+ }))
690
+ .sort((left, right) => left.id.localeCompare(right.id)),
691
+ workstreams: (workstreams || [])
692
+ .map((workstream) => ({
693
+ id: String(workstream.id || '').trim(),
694
+ updatedAt: String(workstream.updatedAt || workstream.createdAt || '').trim(),
695
+ initiativeId: typeof workstream.initiativeId === 'string' ? workstream.initiativeId.trim() : '',
696
+ isArchived: Boolean(workstream.isArchived)
697
+ }))
698
+ .sort((left, right) => left.id.localeCompare(right.id)),
699
+ taskEvents: flattenTaskEvents(tasks || [], archivedTasks || [])
700
+ .map((event) => ({
701
+ id: String(event.id || '').trim(),
702
+ taskId: String(event.taskId || '').trim(),
703
+ createdAt: String(event.createdAt || '').trim(),
704
+ action: String(event.action || '').trim(),
705
+ }))
706
+ .sort((left, right) => left.id.localeCompare(right.id)),
707
+ taxonomies: Array.isArray(taxonomies) ? taxonomies : [],
708
+ taxonomyState: taxonomyState && typeof taxonomyState === 'object' ? taxonomyState : null
709
+ }), [tasks, archivedTasks, initiatives, workstreams, taxonomies, taxonomyState]);
817
710
  // Fingerprint of per-task attachment counts — changes when the MCP tool saves a new
818
711
  // attachment and the React app polls the updated task. Used to trigger an immediate
819
712
  // asset snapshot refresh without requiring a cross-process DOM event.
@@ -1172,6 +1065,9 @@ export function useSyncOrchestrator(input) {
1172
1065
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map(workspaceSyncAnnotatedAttachmentSessionsRef.current
1173
1066
  .map((entry) => [String(entry?.id || '').trim(), String(entry?.updatedAt || entry?.createdAt || '').trim()])
1174
1067
  .filter(([id]) => id.length > 0));
1068
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map(flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current)
1069
+ .map((entry) => [String(entry?.id || '').trim(), String(entry?.createdAt || '').trim()])
1070
+ .filter(([id]) => id.length > 0));
1175
1071
  }, []);
1176
1072
  const refreshLocalWorkspaceTaskCollections = useCallback(async () => {
1177
1073
  const [tasksRes, archiveRes] = await Promise.all([
@@ -1208,543 +1104,173 @@ export function useSyncOrchestrator(input) {
1208
1104
  });
1209
1105
  }, [currentWorkspaceId, fetchPlanningEntities, setArchivedTasks, setTasks]);
1210
1106
  // === SECTION: Auth failure handling & sync readiness ===
1211
- const handleSyncAuthFailure = useCallback(async (source, statusCode) => {
1212
- clearWorkspaceRetry();
1213
- setWorkspaceSyncBusy(false);
1214
- setUserGlobalSyncStatus('error');
1215
- const message = 'Session expired. Sign in again to resume cloud sync.';
1216
- setUserGlobalSyncError(message);
1217
- setWorkspaceLastErrorMessage(message);
1218
- setWorkspaceLastErrorAt(new Date().toISOString());
1219
- reportSyncEvent(currentWorkspaceId, {
1220
- eventType: source === 'handshake' ? 'handshake' : source,
1221
- status: 'error',
1222
- statusCode,
1223
- errorMessage: message
1224
- });
1225
- setAuthBlocked(true);
1226
- setIsAuthenticated(false);
1227
- await checkAuthSession();
1228
- }, [
1107
+ const { handleSyncAuthFailure, ensureWorkspaceSyncReady } = useMemo(() => createSyncAuthCoordinator({
1108
+ currentWorkspaceId,
1109
+ workspaceSyncPhase,
1229
1110
  clearWorkspaceRetry,
1111
+ setWorkspaceSyncBusy,
1112
+ setUserGlobalSyncStatus,
1113
+ setUserGlobalSyncError: (value) => setUserGlobalSyncError(value),
1114
+ setWorkspaceLastErrorMessage: (value) => setWorkspaceLastErrorMessage(value),
1115
+ setWorkspaceLastErrorAt: (value) => setWorkspaceLastErrorAt(value),
1230
1116
  reportSyncEvent,
1231
- currentWorkspaceId,
1232
1117
  setAuthBlocked,
1233
1118
  setIsAuthenticated,
1234
- checkAuthSession
1235
- ]);
1236
- const ensureWorkspaceSyncReady = useCallback(async () => {
1237
- if (!isValidWorkspaceKey(currentWorkspaceId)) {
1238
- const message = 'Workspace sync blocked: local workspace ID is unresolved.';
1239
- setWorkspaceLastErrorMessage(message);
1240
- setWorkspaceLastErrorAt(new Date().toISOString());
1241
- setUserGlobalSyncError(message);
1242
- setUserGlobalSyncStatus('error');
1243
- return false;
1244
- }
1245
- const ensured = await ensureCloudWorkspaceReadyForSync();
1246
- if (ensured.success)
1247
- return true;
1248
- if (ensured.statusCode === 401) {
1249
- await handleSyncAuthFailure('handshake', 401);
1250
- return false;
1251
- }
1252
- const message = ensured.error || 'Workspace sync handshake failed.';
1253
- setWorkspaceLastErrorMessage(message);
1254
- setWorkspaceLastErrorAt(new Date().toISOString());
1255
- setUserGlobalSyncError(message);
1256
- setUserGlobalSyncStatus('error');
1257
- reportSyncEvent(currentWorkspaceId, {
1258
- eventType: 'handshake',
1259
- status: 'error',
1260
- statusCode: ensured.statusCode,
1261
- errorMessage: message
1262
- });
1263
- if (ensured.transient || isTransientWorkspaceSyncStatus(ensured.statusCode)) {
1264
- scheduleWorkspaceSyncRetry(ensured.retryAfterMs);
1265
- }
1266
- else if (workspaceSyncPhase === 'active') {
1267
- persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
1268
- }
1269
- return false;
1270
- }, [
1271
- currentWorkspaceId,
1119
+ checkAuthSession,
1272
1120
  ensureCloudWorkspaceReadyForSync,
1273
- handleSyncAuthFailure,
1274
- reportSyncEvent,
1275
1121
  scheduleWorkspaceSyncRetry,
1122
+ persistWorkspaceSyncPatchSafely
1123
+ }), [
1124
+ currentWorkspaceId,
1276
1125
  workspaceSyncPhase,
1126
+ clearWorkspaceRetry,
1127
+ reportSyncEvent,
1128
+ setAuthBlocked,
1129
+ setIsAuthenticated,
1130
+ checkAuthSession,
1131
+ ensureCloudWorkspaceReadyForSync,
1132
+ scheduleWorkspaceSyncRetry,
1277
1133
  persistWorkspaceSyncPatchSafely
1278
1134
  ]);
1279
- // === SECTION: Push sync ===
1280
- const pushWorkspaceChangesToCloud = useCallback(async (signature, options) => {
1281
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1282
- return false;
1283
- if (!isValidWorkspaceKey(currentWorkspaceId))
1284
- return false;
1285
- if (workspaceRepairModeRef.current === true && options?.repairMode !== true) {
1286
- workspacePendingSignatureRef.current = signature;
1287
- return false;
1288
- }
1289
- if (workspaceSyncPhase === 'attach-cloud') {
1290
- workspacePendingSignatureRef.current = signature;
1291
- return false;
1292
- }
1293
- if (!isAuthenticated) {
1294
- const authed = await checkAuthSession();
1295
- if (!authed)
1296
- return false;
1297
- }
1298
- if (shouldSuppressAttachBootstrapPush(signature)) {
1299
- return false;
1300
- }
1301
- if (workspacePushInFlightRef.current || workspacePullInFlightRef.current) {
1302
- workspacePendingSignatureRef.current = signature;
1303
- return false;
1304
- }
1305
- const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'push');
1306
- if (!leaseAcquired) {
1307
- workspacePendingSignatureRef.current = signature;
1308
- reportWorkspaceWindowContention('push');
1309
- return false;
1310
- }
1311
- workspacePushInFlightRef.current = true;
1312
- setWorkspaceSyncBusy(true);
1313
- setUserGlobalSyncStatus('syncing');
1314
- setUserGlobalSyncError(null);
1315
- const pushStartedAtMs = Date.now();
1316
- const forceAllAiProfiles = options?.forceAllAiProfiles === true || workspaceForceFullAiProfilePushRef.current === true;
1317
- try {
1318
- await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1319
- await refreshWorkspaceSyncAiProfilesSnapshot();
1320
- await refreshWorkspaceSyncAssetsSnapshot();
1321
- await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
1322
- const ready = await ensureWorkspaceSyncReady();
1323
- if (!ready)
1324
- return false;
1325
- const payload = buildWorkspaceSyncPayload({ forceAllAiProfiles });
1326
- setWorkspaceSyncPendingChanges(payload.changes.length);
1327
- const pushResult = await runWorkspacePushSyncService({
1328
- resolveCloudAuthUrl,
1329
- workspaceId: currentWorkspaceId,
1330
- payload,
1331
- ensureCloudWorkspaceReadyForSync,
1332
- repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true
1333
- });
1334
- if (!pushResult.success) {
1335
- if (pushResult.status === 401) {
1336
- await handleSyncAuthFailure('push', 401);
1337
- return false;
1338
- }
1339
- let message = pushResult.error
1340
- ? `Workspace sync push failed (${pushResult.status || 0}): ${pushResult.error}`
1341
- : `Workspace sync push failed (${pushResult.status || 0})`;
1342
- if (pushResult.status === 413) {
1343
- message = 'Workspace sync failed because the payload is too large. This usually happens when one or more attachments exceed the project limit.';
1344
- }
1345
- setUserGlobalSyncStatus('error');
1346
- setUserGlobalSyncError(message);
1347
- setWorkspaceLastErrorMessage(message);
1348
- setWorkspaceLastErrorAt(new Date().toISOString());
1349
- reportSyncEvent(currentWorkspaceId, {
1350
- eventType: 'push',
1351
- status: 'error',
1352
- statusCode: pushResult.status,
1353
- errorMessage: message,
1354
- requestMs: pushResult.requestMs
1355
- });
1356
- workspacePendingSignatureRef.current = signature;
1357
- if (pushResult.transient || isTransientWorkspaceSyncStatus(pushResult.status)) {
1358
- scheduleWorkspaceSyncRetry(pushResult.retryAfterMs);
1359
- }
1360
- else if (workspaceSyncPhase === 'active') {
1361
- persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
1362
- }
1363
- return false;
1364
- }
1365
- workspaceLastPushedSignatureRef.current = signature;
1366
- workspaceLastPushedTaskIdsRef.current = pushResult.currentTaskIds;
1367
- workspaceLastPushedInitiativeIdsRef.current = new Set(pushResult.currentInitiativeIds);
1368
- workspaceLastPushedInitiativeWatermarksRef.current = new Map(pushResult.currentInitiativeWatermarks);
1369
- workspaceLastPushedWorkstreamIdsRef.current = new Set(pushResult.currentWorkstreamIds);
1370
- workspaceLastPushedWorkstreamWatermarksRef.current = new Map(pushResult.currentWorkstreamWatermarks);
1371
- workspaceLastPushedAiProfileIdsRef.current = new Set(pushResult.currentAiProfileIds);
1372
- workspaceLastPushedAiProfileWatermarksRef.current = new Map(pushResult.currentAiProfileWatermarks);
1373
- workspaceLastPushedDocumentPathsRef.current = new Set(pushResult.currentDocumentPaths);
1374
- workspaceLastPushedDocumentWatermarksRef.current = new Map(pushResult.currentDocumentWatermarks);
1375
- workspaceLastPushedAssetPathsRef.current = new Set(pushResult.currentAssetPaths);
1376
- workspaceLastPushedAssetWatermarksRef.current = new Map(pushResult.currentAssetWatermarks);
1377
- workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set(pushResult.currentDocumentReviewSessionIds);
1378
- workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map(pushResult.currentDocumentReviewSessionWatermarks);
1379
- workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set(pushResult.currentDocumentReviewCommentIds);
1380
- workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map(pushResult.currentDocumentReviewCommentWatermarks);
1381
- workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set(pushResult.currentAnnotatedAttachmentSessionIds);
1382
- workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map(pushResult.currentAnnotatedAttachmentSessionWatermarks);
1383
- if (forceAllAiProfiles) {
1384
- workspaceForceFullAiProfilePushRef.current = false;
1385
- }
1386
- for (const [taskId, watermark] of pushResult.pushedWatermarks) {
1387
- workspaceLastPushedWatermarksRef.current.set(taskId, watermark);
1388
- }
1389
- if (pushResult.deleteTaskIds.size > 0) {
1390
- for (const taskId of pushResult.deleteTaskIds) {
1391
- workspaceDeletedTaskIdsRef.current.delete(taskId);
1392
- workspaceLastPushedWatermarksRef.current.delete(taskId);
1393
- }
1394
- }
1395
- // Persist watermarks so a page reload doesn't trigger a full resync
1396
- savePersistedSyncWatermarks(currentWorkspaceId, {
1397
- taskWatermarks: workspaceLastPushedWatermarksRef.current,
1398
- initiativeWatermarks: workspaceLastPushedInitiativeWatermarksRef.current,
1399
- workstreamWatermarks: workspaceLastPushedWorkstreamWatermarksRef.current,
1400
- documentWatermarks: workspaceLastPushedDocumentWatermarksRef.current,
1401
- assetWatermarks: workspaceLastPushedAssetWatermarksRef.current,
1402
- documentReviewSessionWatermarks: workspaceLastPushedDocumentReviewSessionWatermarksRef.current,
1403
- documentReviewCommentWatermarks: workspaceLastPushedDocumentReviewCommentWatermarksRef.current,
1404
- annotatedAttachmentSessionWatermarks: workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current,
1405
- taxonomyStateFingerprint
1406
- });
1407
- if (Array.isArray(pushResult.emittedEventIds) && pushResult.emittedEventIds.length > 0) {
1408
- for (const rawEventId of pushResult.emittedEventIds) {
1409
- const eventId = String(rawEventId || '').trim();
1410
- if (!eventId || realtimeSuppressedEventIdsRef.current.has(eventId))
1411
- continue;
1412
- realtimeSuppressedEventIdsRef.current.add(eventId);
1413
- realtimeSuppressedEventQueueRef.current.push(eventId);
1414
- }
1415
- while (realtimeSuppressedEventQueueRef.current.length > 2048) {
1416
- const stale = realtimeSuppressedEventQueueRef.current.shift();
1417
- if (stale)
1418
- realtimeSuppressedEventIdsRef.current.delete(stale);
1419
- }
1420
- }
1421
- clearWorkspaceRetry();
1422
- clearWorkspaceIssue();
1423
- const syncedAt = pushResult.syncedAt || new Date().toISOString();
1424
- setWorkspaceLastPushAt(syncedAt);
1425
- setWorkspaceSyncPendingChanges(0);
1426
- setUserGlobalSyncStatus('idle');
1427
- persistWorkspaceSyncPatchSafely({
1428
- phase: 'active',
1429
- lastPushAt: syncedAt,
1430
- lastSyncedAt: syncedAt,
1431
- lastErrorMessage: null
1432
- });
1433
- reportSyncEvent(currentWorkspaceId, {
1434
- eventType: workspaceSyncPhase === 'provision-local' ? 'bootstrap' : 'push',
1435
- status: 'success',
1436
- changeCount: pushResult.changeCount,
1437
- requestMs: pushResult.requestMs
1438
- });
1439
- return true;
1440
- }
1441
- catch (error) {
1442
- const message = 'Workspace sync push failed.';
1443
- setUserGlobalSyncStatus('error');
1444
- setUserGlobalSyncError(message);
1445
- setWorkspaceLastErrorMessage(message);
1446
- setWorkspaceLastErrorAt(new Date().toISOString());
1447
- workspacePendingSignatureRef.current = signature;
1448
- scheduleWorkspaceSyncRetry();
1449
- reportSyncEvent(currentWorkspaceId, {
1450
- eventType: 'push',
1451
- status: 'error',
1452
- errorMessage: `${message} ${String(error?.message || '').trim()}`.trim()
1453
- });
1454
- logSyncDebug('push_failed_exception', {
1455
- workspaceId: currentWorkspaceId,
1456
- elapsedMs: Date.now() - pushStartedAtMs
1457
- });
1458
- return false;
1459
- }
1460
- finally {
1461
- workspacePushInFlightRef.current = false;
1462
- setWorkspaceSyncBusy(workspacePullInFlightRef.current);
1463
- releaseWorkspaceSyncLease(currentWorkspaceId);
1464
- const pendingSignature = workspacePendingSignatureRef.current;
1465
- if (pendingSignature
1466
- && pendingSignature !== workspaceLastPushedSignatureRef.current
1467
- && !workspacePullInFlightRef.current) {
1468
- workspacePendingSignatureRef.current = '';
1469
- window.setTimeout(() => {
1470
- void pushWorkspaceChangesToCloud(pendingSignature);
1471
- }, 120);
1472
- }
1473
- }
1474
- }, [
1135
+ // === SECTION: Push/Pull sync ===
1136
+ const transferCoordinator = useMemo(() => createWorkspaceTransferCoordinator({
1475
1137
  cloudAuthConfigured,
1476
1138
  runtimeMode,
1477
1139
  workspaceCloudSyncEnabled,
1478
1140
  currentWorkspaceId,
1479
1141
  workspaceSyncPhase,
1142
+ taxonomyStateFingerprint,
1480
1143
  isAuthenticated,
1481
1144
  checkAuthSession,
1482
- clearWorkspaceIssue,
1483
- refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1484
- refreshWorkspaceSyncAiProfilesSnapshot,
1485
- refreshWorkspaceSyncAssetsSnapshot,
1486
- refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1487
- ensureWorkspaceSyncReady,
1488
- buildWorkspaceSyncPayload,
1489
1145
  resolveCloudAuthUrl,
1490
1146
  ensureCloudWorkspaceReadyForSync,
1491
- shouldSuppressAttachBootstrapPush,
1147
+ ensureWorkspaceSyncReady,
1492
1148
  handleSyncAuthFailure,
1493
1149
  clearWorkspaceRetry,
1494
- persistWorkspaceSyncPatch,
1495
- reportSyncEvent,
1150
+ clearWorkspaceIssue,
1151
+ isWorkspaceRetryPending,
1496
1152
  scheduleWorkspaceSyncRetry,
1153
+ persistWorkspaceSyncPatchSafely,
1154
+ persistWorkspaceSyncPatchBestEffort,
1155
+ reportSyncEvent,
1497
1156
  acquireWorkspaceSyncLease,
1498
1157
  releaseWorkspaceSyncLease,
1499
1158
  reportWorkspaceWindowContention,
1159
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1160
+ refreshWorkspaceSyncAiProfilesSnapshot,
1161
+ refreshWorkspaceSyncAssetsSnapshot,
1162
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1163
+ refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
1164
+ refreshLocalWorkspaceTaskCollections,
1165
+ buildWorkspaceSyncPayload,
1166
+ buildWorkspaceSyncSignature,
1167
+ seedWorkspaceSnapshotPushBaseline,
1168
+ shouldSuppressAttachBootstrapPush,
1169
+ recordSuppressedEventIds: (...args) => realtimeBridgeRef.current.recordSuppressedEventIds(...args),
1170
+ workspaceRepairModeRef,
1171
+ workspaceForceFullAiProfilePushRef,
1500
1172
  workspacePushInFlightRef,
1501
1173
  workspacePullInFlightRef,
1502
- persistWorkspaceSyncPatchSafely
1503
- ]);
1504
- // === SECTION: Pull sync ===
1505
- const pullWorkspaceChangesFromCloud = useCallback(async (options) => {
1506
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1507
- return false;
1508
- if (!isValidWorkspaceKey(currentWorkspaceId))
1509
- return false;
1510
- if (workspaceRepairModeRef.current === true && options?.repairMode !== true)
1511
- return false;
1512
- if (workspaceSyncPhase === 'provision-local')
1513
- return false;
1514
- if (isWorkspaceRetryPending())
1515
- return false;
1516
- if (!isAuthenticated) {
1517
- const authed = await checkAuthSession();
1518
- if (!authed)
1519
- return false;
1520
- }
1521
- if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
1522
- return false;
1523
- const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'pull');
1524
- if (!leaseAcquired) {
1525
- reportWorkspaceWindowContention('pull');
1526
- return false;
1527
- }
1528
- workspacePullInFlightRef.current = true;
1529
- setWorkspaceSyncBusy(true);
1530
- setUserGlobalSyncStatus('syncing');
1531
- setUserGlobalSyncError(null);
1532
- const pullStartedAtMs = Date.now();
1533
- try {
1534
- const ready = await ensureWorkspaceSyncReady();
1535
- if (!ready)
1536
- return false;
1537
- const bootstrap = workspaceSyncPhase === 'attach-cloud';
1538
- const cursor = bootstrap ? null : workspacePullCursorRef.current;
1539
- const pullResult = await runWorkspacePullSyncService({
1540
- resolveCloudAuthUrl,
1541
- workspaceId: currentWorkspaceId,
1542
- cursor,
1543
- bootstrap,
1544
- ensureCloudWorkspaceReadyForSync,
1545
- maxPages: 20,
1546
- repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true,
1547
- onPagePulled: () => { bootstrapLastProgressAtRef.current = Date.now(); },
1548
- onChangesApplied: () => { bootstrapLastProgressAtRef.current = Date.now(); }
1549
- });
1550
- if (!pullResult.success) {
1551
- if (pullResult.kind === 'pull_http' && pullResult.status === 401) {
1552
- await handleSyncAuthFailure('pull', 401);
1553
- return false;
1554
- }
1555
- if (pullResult.kind === 'apply_auth') {
1556
- await handleSyncAuthFailure('apply', 401);
1557
- return false;
1558
- }
1559
- let message = pullResult.kind === 'apply_payload' || pullResult.kind === 'apply_all_candidates' || pullResult.kind === 'exception'
1560
- ? String(pullResult.error || 'Workspace sync apply failed.')
1561
- : `Workspace sync pull failed (${pullResult.status || 0})`;
1562
- if (pullResult.status === 413) {
1563
- message = 'Local sync failed because the downloaded payload is too large. This usually happens when the workspace contains massive attachments.';
1564
- }
1565
- setUserGlobalSyncStatus('error');
1566
- setUserGlobalSyncError(message);
1567
- setWorkspaceLastErrorMessage(message);
1568
- setWorkspaceLastErrorAt(new Date().toISOString());
1569
- reportSyncEvent(currentWorkspaceId, {
1570
- eventType: pullResult.kind?.startsWith('apply') ? 'apply' : 'pull',
1571
- status: 'error',
1572
- statusCode: pullResult.status,
1573
- errorMessage: message,
1574
- requestMs: pullResult.pullRequestMs
1575
- });
1576
- if (pullResult.transient || pullResult.hasTransientApplyFailure || isTransientWorkspaceSyncStatus(pullResult.status)) {
1577
- scheduleWorkspaceSyncRetry(pullResult.retryAfterMs);
1578
- }
1579
- else {
1580
- persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
1581
- }
1582
- return false;
1583
- }
1584
- if (Array.isArray(pullResult.emittedEventIds)) {
1585
- for (const eventId of pullResult.emittedEventIds) {
1586
- if (eventId) {
1587
- realtimeSuppressedEventIdsRef.current.add(eventId);
1588
- // Optional: cleanup after a generous delay if WebSocket never arrives to clear it
1589
- setTimeout(() => {
1590
- realtimeSuppressedEventIdsRef.current.delete(eventId);
1591
- }, 60000);
1592
- }
1593
- }
1594
- }
1595
- if (pullResult.pulledDeleteTaskIds.size > 0) {
1596
- setTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
1597
- setArchivedTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
1598
- }
1599
- const decryptFailures = Array.isArray(pullResult.documentDecryptFailures)
1600
- ? pullResult.documentDecryptFailures.filter((value) => String(value || '').trim().length > 0)
1601
- : [];
1602
- if (decryptFailures.length > 0) {
1603
- logSyncDebug('pull_document_decrypt_failures', {
1604
- workspaceId: currentWorkspaceId,
1605
- failureCount: decryptFailures.length
1606
- });
1607
- }
1608
- workspacePullCursorRef.current = pullResult.cursor || null;
1609
- if (pullResult.appliedChanges > 0 && (pullResult.pulledActiveUpserts || pullResult.pulledArchivedUpserts)) {
1610
- try {
1611
- await refreshLocalWorkspaceTaskCollections();
1612
- }
1613
- catch {
1614
- logSyncDebug('pull_local_refresh_failed', {
1615
- workspaceId: currentWorkspaceId,
1616
- appliedChanges: pullResult.appliedChanges
1617
- });
1618
- }
1619
- }
1620
- clearWorkspaceRetry();
1621
- clearWorkspaceIssue();
1622
- const syncedAt = pullResult.syncedAt || new Date().toISOString();
1623
- setWorkspaceLastPullAt(syncedAt);
1624
- setWorkspaceSyncPendingChanges(0);
1625
- setUserGlobalSyncStatus('idle');
1626
- if (workspaceSyncPhase === 'attach-cloud') {
1627
- workspaceCaptureAttachBootstrapBaselineRef.current = true;
1628
- workspacePendingSignatureRef.current = '';
1629
- }
1630
- await persistWorkspaceSyncPatch({
1631
- phase: 'active',
1632
- pullCursor: workspacePullCursorRef.current,
1633
- lastPullAt: syncedAt,
1634
- lastSyncedAt: syncedAt,
1635
- lastErrorMessage: null
1636
- });
1637
- reportSyncEvent(currentWorkspaceId, {
1638
- eventType: workspaceSyncPhase === 'attach-cloud' ? 'bootstrap' : 'pull',
1639
- status: 'success',
1640
- changeCount: pullResult.appliedChanges,
1641
- requestMs: pullResult.pullRequestMs
1642
- });
1643
- if (workspaceSyncPhase === 'attach-cloud') {
1644
- await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1645
- await refreshWorkspaceSyncAssetsSnapshot();
1646
- await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
1647
- await refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
1648
- seedWorkspaceSnapshotPushBaseline();
1649
- return true;
1650
- }
1651
- const pendingSignature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
1652
- if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
1653
- workspacePendingSignatureRef.current = '';
1654
- window.setTimeout(() => {
1655
- void pushWorkspaceChangesToCloud(pendingSignature);
1656
- }, 120);
1657
- }
1658
- return true;
1659
- }
1660
- catch (error) {
1661
- const detail = String(error?.message || '').trim();
1662
- const message = detail
1663
- ? `Workspace sync pull failed. ${detail}`
1664
- : 'Workspace sync pull failed.';
1665
- setUserGlobalSyncStatus('error');
1666
- setUserGlobalSyncError(message);
1667
- setWorkspaceLastErrorMessage(message);
1668
- setWorkspaceLastErrorAt(new Date().toISOString());
1669
- scheduleWorkspaceSyncRetry();
1670
- reportSyncEvent(currentWorkspaceId, {
1671
- eventType: 'pull',
1672
- status: 'error',
1673
- errorMessage: message,
1674
- requestMs: Date.now() - pullStartedAtMs
1675
- });
1676
- logSyncDebug('pull_failed_exception', {
1677
- workspaceId: currentWorkspaceId,
1678
- elapsedMs: Date.now() - pullStartedAtMs,
1679
- error: detail || null
1680
- });
1681
- return false;
1682
- }
1683
- finally {
1684
- workspacePullInFlightRef.current = false;
1685
- setWorkspaceSyncBusy(workspacePushInFlightRef.current);
1686
- releaseWorkspaceSyncLease(currentWorkspaceId);
1687
- }
1688
- }, [
1174
+ workspaceLastPushedSignatureRef,
1175
+ workspacePendingSignatureRef,
1176
+ workspaceLastPushedTaskIdsRef,
1177
+ workspaceDeletedTaskIdsRef,
1178
+ workspacePullCursorRef,
1179
+ workspaceLastPushedWatermarksRef,
1180
+ workspaceLastPushedInitiativeIdsRef,
1181
+ workspaceLastPushedInitiativeWatermarksRef,
1182
+ workspaceLastPushedWorkstreamIdsRef,
1183
+ workspaceLastPushedWorkstreamWatermarksRef,
1184
+ workspaceLastPushedAiProfileIdsRef,
1185
+ workspaceLastPushedAiProfileWatermarksRef,
1186
+ workspaceLastPushedDocumentPathsRef,
1187
+ workspaceLastPushedDocumentWatermarksRef,
1188
+ workspaceLastPushedAssetPathsRef,
1189
+ workspaceLastPushedAssetWatermarksRef,
1190
+ workspaceLastPushedDocumentReviewSessionIdsRef,
1191
+ workspaceLastPushedDocumentReviewSessionWatermarksRef,
1192
+ workspaceLastPushedDocumentReviewCommentIdsRef,
1193
+ workspaceLastPushedDocumentReviewCommentWatermarksRef,
1194
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef,
1195
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef,
1196
+ workspaceLastPushedTaskEventWatermarksRef,
1197
+ workspaceCaptureAttachBootstrapBaselineRef,
1198
+ workspaceStartupFullReconcileRanRef,
1199
+ workspaceFullReconcileInFlightRef,
1200
+ bootstrapLastProgressAtRef,
1201
+ realtimeSuppressedEventQueueRef,
1202
+ setWorkspaceSyncBusy,
1203
+ setUserGlobalSyncStatus,
1204
+ setUserGlobalSyncError,
1205
+ setWorkspaceLastErrorMessage,
1206
+ setWorkspaceLastErrorAt,
1207
+ setWorkspaceLastPullAt,
1208
+ setWorkspaceLastPushAt,
1209
+ setWorkspaceSyncPendingChanges,
1210
+ setTasks,
1211
+ setArchivedTasks
1212
+ }), [
1689
1213
  cloudAuthConfigured,
1690
1214
  runtimeMode,
1691
1215
  workspaceCloudSyncEnabled,
1692
1216
  currentWorkspaceId,
1693
1217
  workspaceSyncPhase,
1694
- isWorkspaceRetryPending,
1218
+ taxonomyStateFingerprint,
1695
1219
  isAuthenticated,
1696
1220
  checkAuthSession,
1697
- clearWorkspaceIssue,
1698
- ensureWorkspaceSyncReady,
1699
1221
  resolveCloudAuthUrl,
1700
1222
  ensureCloudWorkspaceReadyForSync,
1223
+ ensureWorkspaceSyncReady,
1701
1224
  handleSyncAuthFailure,
1702
- refreshLocalWorkspaceTaskCollections,
1225
+ clearWorkspaceRetry,
1226
+ clearWorkspaceIssue,
1227
+ scheduleWorkspaceSyncRetry,
1228
+ persistWorkspaceSyncPatchSafely,
1229
+ persistWorkspaceSyncPatchBestEffort,
1230
+ reportSyncEvent,
1231
+ acquireWorkspaceSyncLease,
1232
+ releaseWorkspaceSyncLease,
1233
+ reportWorkspaceWindowContention,
1703
1234
  refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1235
+ refreshWorkspaceSyncAiProfilesSnapshot,
1704
1236
  refreshWorkspaceSyncAssetsSnapshot,
1705
1237
  refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1706
1238
  refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
1707
- clearWorkspaceRetry,
1708
- persistWorkspaceSyncPatch,
1709
- reportSyncEvent,
1239
+ refreshLocalWorkspaceTaskCollections,
1240
+ buildWorkspaceSyncPayload,
1710
1241
  buildWorkspaceSyncSignature,
1711
- pushWorkspaceChangesToCloud,
1712
1242
  seedWorkspaceSnapshotPushBaseline,
1713
- scheduleWorkspaceSyncRetry,
1714
- acquireWorkspaceSyncLease,
1715
- releaseWorkspaceSyncLease,
1716
- reportWorkspaceWindowContention,
1717
- workspacePushInFlightRef,
1718
- workspacePullInFlightRef,
1719
- persistWorkspaceSyncPatchSafely
1243
+ shouldSuppressAttachBootstrapPush,
1244
+ setTasks,
1245
+ setArchivedTasks
1720
1246
  ]);
1721
- // === SECTION: Realtime sync integration ===
1722
- const handleRealtimeSignal = useCallback((signal) => {
1723
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1724
- return;
1725
- if (workspaceSyncPhase === 'provision-local')
1726
- return;
1727
- const eventId = String(signal?.eventId || '').trim();
1728
- if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
1729
- return;
1730
- if (isWorkspaceRetryPending() || workspacePullInFlightRef.current)
1731
- return;
1732
- if (realtimePullDebounceRef.current !== null) {
1733
- window.clearTimeout(realtimePullDebounceRef.current);
1734
- }
1735
- realtimePullDebounceRef.current = window.setTimeout(() => {
1736
- realtimePullDebounceRef.current = null;
1737
- void pullWorkspaceChangesFromCloud();
1738
- }, 180);
1739
- }, [
1247
+ const { pushWorkspaceChangesToCloud, pullWorkspaceChangesFromCloud } = transferCoordinator;
1248
+ const realtimeBridgeRef = useRef({
1249
+ handleRealtimeSignal: () => { },
1250
+ recordSuppressedEventIds: () => { },
1251
+ clearRealtimePullDebounce: () => { }
1252
+ });
1253
+ const realtimeBridge = useMemo(() => createRealtimeSyncBridge({
1740
1254
  cloudAuthConfigured,
1741
1255
  runtimeMode,
1742
1256
  workspaceCloudSyncEnabled,
1743
1257
  workspaceSyncPhase,
1744
- isWorkspaceRetryPending,
1745
1258
  workspacePullInFlightRef,
1259
+ realtimePullDebounceRef,
1260
+ realtimeSuppressedEventIdsRef,
1261
+ clearWorkspaceRetry,
1262
+ isWorkspaceRetryPending,
1263
+ pullWorkspaceChangesFromCloud
1264
+ }), [
1265
+ cloudAuthConfigured,
1266
+ runtimeMode,
1267
+ workspaceCloudSyncEnabled,
1268
+ workspaceSyncPhase,
1269
+ clearWorkspaceRetry,
1270
+ isWorkspaceRetryPending,
1746
1271
  pullWorkspaceChangesFromCloud
1747
1272
  ]);
1273
+ realtimeBridgeRef.current = realtimeBridge;
1748
1274
  const realtimeSocketUrl = resolveWebSocketUrl('/taskforce-ws');
1749
1275
  const normalizedRealtimeUserId = String(authUserId || '').trim();
1750
1276
  const realtimeEnabled = Boolean(realtimeSyncEnabled
@@ -1760,7 +1286,7 @@ export function useSyncOrchestrator(input) {
1760
1286
  enabled: realtimeEnabled,
1761
1287
  workspaceId: currentWorkspaceId,
1762
1288
  websocketUrl: realtimeSocketUrl,
1763
- onSignal: handleRealtimeSignal,
1289
+ onSignal: realtimeBridge.handleRealtimeSignal,
1764
1290
  onTelemetry: setRealtimeTelemetry,
1765
1291
  userId: normalizedRealtimeUserId || undefined
1766
1292
  });
@@ -1768,39 +1294,9 @@ export function useSyncOrchestrator(input) {
1768
1294
  setRealtimeConnectionState(realtimeSync.connectionState);
1769
1295
  }, [realtimeSync.connectionState]);
1770
1296
  useEffect(() => {
1771
- return () => {
1772
- if (realtimePullDebounceRef.current !== null) {
1773
- window.clearTimeout(realtimePullDebounceRef.current);
1774
- realtimePullDebounceRef.current = null;
1775
- }
1776
- };
1777
- }, []);
1297
+ return () => realtimeBridge.clearRealtimePullDebounce();
1298
+ }, [realtimeBridge]);
1778
1299
  // === SECTION: Repair & manual retry ===
1779
- const fetchLocalWorkspaceSnapshot = useCallback(async () => {
1780
- const [localTasksRes, localArchiveRes] = await Promise.all([
1781
- fetch('/api/taskforce/tasks', {
1782
- method: 'GET',
1783
- credentials: 'include'
1784
- }),
1785
- fetch('/api/taskforce/archive', {
1786
- method: 'GET',
1787
- credentials: 'include'
1788
- })
1789
- ]);
1790
- if (!localTasksRes.ok || !localArchiveRes.ok) {
1791
- return {
1792
- tasks: [],
1793
- archived: [],
1794
- error: 'Failed to read local workspace snapshot before sync bootstrap.'
1795
- };
1796
- }
1797
- const localTasksPayload = await localTasksRes.json().catch(() => ({}));
1798
- const localArchivePayload = await localArchiveRes.json().catch(() => ({}));
1799
- return {
1800
- tasks: Array.isArray(localTasksPayload?.tasks) ? localTasksPayload.tasks : [],
1801
- archived: Array.isArray(localArchivePayload?.archived) ? localArchivePayload.archived : []
1802
- };
1803
- }, []);
1804
1300
  const saveWorkspaceCloudSyncSettings = useCallback(async (settings) => {
1805
1301
  return saveWorkspaceCloudSyncSettingsBase(settings, {
1806
1302
  cloudAuthConfigured,
@@ -1816,144 +1312,79 @@ export function useSyncOrchestrator(input) {
1816
1312
  const retryUserGlobalSettingsSync = useCallback(async () => {
1817
1313
  await syncUserGlobalSettings({ preferCloudOnFirstSync: false });
1818
1314
  }, [syncUserGlobalSettings]);
1819
- const retryWorkspaceCloudSync = useCallback(async () => {
1820
- clearWorkspaceRetry();
1821
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1822
- return false;
1823
- if (!isAuthenticated) {
1824
- const authed = await checkAuthSession();
1825
- if (!authed)
1826
- return false;
1827
- }
1828
- workspaceForceFullAiProfilePushRef.current = true;
1829
- if (workspaceSyncPhase === 'provision-local') {
1830
- const signature = buildWorkspaceSyncSignature();
1831
- if (!signature)
1832
- return false;
1833
- return pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true });
1834
- }
1835
- const pulled = await pullWorkspaceChangesFromCloud();
1836
- if (workspaceSyncPhase === 'attach-cloud')
1837
- return pulled;
1838
- const signature = buildWorkspaceSyncSignature();
1839
- const forceAllAiProfiles = workspaceForceFullAiProfilePushRef.current === true;
1840
- if (!forceAllAiProfiles && shouldSuppressAttachBootstrapPush(signature)) {
1841
- return pulled;
1842
- }
1843
- if (signature) {
1844
- workspacePendingSignatureRef.current = signature;
1845
- const pushed = await pushWorkspaceChangesToCloud(signature, {
1846
- forceAllAiProfiles
1847
- });
1848
- return pulled || pushed;
1849
- }
1850
- return pulled;
1851
- }, [
1852
- clearWorkspaceRetry,
1853
- cloudAuthConfigured,
1854
- runtimeMode,
1315
+ const recoveryCoordinator = useMemo(() => createSyncRecoveryCoordinator({
1316
+ currentWorkspaceId,
1317
+ taxonomyStateFingerprint,
1855
1318
  workspaceCloudSyncEnabled,
1856
- isAuthenticated,
1857
- checkAuthSession,
1858
1319
  workspaceSyncPhase,
1859
- buildWorkspaceSyncSignature,
1860
- shouldSuppressAttachBootstrapPush,
1861
- pushWorkspaceChangesToCloud,
1862
- pullWorkspaceChangesFromCloud
1863
- ]);
1864
- const resetWorkspaceSyncCursorAndPull = useCallback(async () => {
1865
- const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
1866
- if (!leaseAcquired) {
1867
- forceClearWorkspaceSyncLease(currentWorkspaceId);
1868
- const reclaimed = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
1869
- if (!reclaimed) {
1870
- reportWorkspaceWindowContention('repair');
1871
- return;
1872
- }
1873
- }
1874
- clearWorkspaceRetry();
1875
- clearWorkspaceIssue();
1876
- bootstrapPhaseStartedAtRef.current = null;
1877
- bootstrapLastProgressAtRef.current = null;
1878
- workspacePullCursorRef.current = null;
1879
- workspaceAttachBootstrapBaselineSignatureRef.current = '';
1880
- workspaceCaptureAttachBootstrapBaselineRef.current = false;
1881
- workspacePendingSignatureRef.current = '';
1882
- workspaceLastPushedSignatureRef.current = '';
1883
- workspaceLastPushedWatermarksRef.current = new Map();
1884
- workspaceLastPushedInitiativeIdsRef.current = new Set();
1885
- workspaceLastPushedInitiativeWatermarksRef.current = new Map();
1886
- workspaceLastPushedWorkstreamIdsRef.current = new Set();
1887
- workspaceLastPushedWorkstreamWatermarksRef.current = new Map();
1888
- workspaceLastPushedAiProfileIdsRef.current = new Set();
1889
- workspaceLastPushedAiProfileWatermarksRef.current = new Map();
1890
- workspaceLastPushedDocumentPathsRef.current = new Set();
1891
- workspaceLastPushedDocumentWatermarksRef.current = new Map();
1892
- workspaceLastPushedAssetPathsRef.current = new Set();
1893
- workspaceLastPushedAssetWatermarksRef.current = new Map();
1894
- workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set();
1895
- workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map();
1896
- workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set();
1897
- workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
1898
- workspaceForceFullAiProfilePushRef.current = false;
1899
- workspaceStartupRepairRanRef.current = false;
1900
- // Clear persisted watermarks so the next push is a full resync (repair intent)
1901
- savePersistedSyncWatermarks(currentWorkspaceId, {
1902
- taskWatermarks: new Map(),
1903
- initiativeWatermarks: new Map(),
1904
- workstreamWatermarks: new Map(),
1905
- documentWatermarks: new Map(),
1906
- assetWatermarks: new Map(),
1907
- documentReviewSessionWatermarks: new Map(),
1908
- documentReviewCommentWatermarks: new Map(),
1909
- annotatedAttachmentSessionWatermarks: new Map(),
1910
- taxonomyStateFingerprint
1911
- });
1912
- try {
1913
- if (!workspaceCloudSyncEnabled)
1914
- return;
1915
- workspaceRepairModeRef.current = true;
1916
- const repairPhase = workspaceSyncPhase === 'provision-local'
1917
- ? 'provision-local'
1918
- : workspaceSyncPhase === 'attach-cloud'
1919
- ? 'attach-cloud'
1920
- : lastBootstrapPhaseRef.current === 'provision-local' || lastBootstrapPhaseRef.current === 'attach-cloud'
1921
- ? lastBootstrapPhaseRef.current
1922
- : (workspaceLastPullAt ? 'attach-cloud' : 'provision-local');
1923
- await persistWorkspaceSyncPatch({
1924
- phase: repairPhase,
1925
- pullCursor: null,
1926
- lastErrorMessage: null
1927
- });
1928
- if (repairPhase === 'provision-local') {
1929
- const signature = buildWorkspaceSyncSignature();
1930
- if (!signature)
1931
- return;
1932
- await pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true, repairMode: true });
1933
- return;
1934
- }
1935
- await pullWorkspaceChangesFromCloud({ repairMode: true });
1936
- }
1937
- finally {
1938
- workspaceRepairModeRef.current = false;
1939
- releaseWorkspaceSyncLease(currentWorkspaceId);
1940
- }
1941
- }, [
1320
+ workspaceLastPullAt,
1321
+ lastBootstrapPhaseRef,
1322
+ workspaceRepairModeRef,
1323
+ workspaceForceFullAiProfilePushRef,
1324
+ workspaceStartupRepairRanRef,
1325
+ workspaceStartupFullReconcileRanRef,
1326
+ workspaceFullReconcileInFlightRef,
1327
+ workspacePullCursorRef,
1328
+ workspaceAttachBootstrapBaselineSignatureRef,
1329
+ workspaceCaptureAttachBootstrapBaselineRef,
1330
+ workspacePendingSignatureRef,
1331
+ workspaceLastPushedSignatureRef,
1332
+ workspaceLastPushedWatermarksRef,
1333
+ workspaceLastPushedInitiativeIdsRef,
1334
+ workspaceLastPushedInitiativeWatermarksRef,
1335
+ workspaceLastPushedWorkstreamIdsRef,
1336
+ workspaceLastPushedWorkstreamWatermarksRef,
1337
+ workspaceLastPushedAiProfileIdsRef,
1338
+ workspaceLastPushedAiProfileWatermarksRef,
1339
+ workspaceLastPushedDocumentPathsRef,
1340
+ workspaceLastPushedDocumentWatermarksRef,
1341
+ workspaceLastPushedAssetPathsRef,
1342
+ workspaceLastPushedAssetWatermarksRef,
1343
+ workspaceLastPushedDocumentReviewSessionIdsRef,
1344
+ workspaceLastPushedDocumentReviewSessionWatermarksRef,
1345
+ workspaceLastPushedDocumentReviewCommentIdsRef,
1346
+ workspaceLastPushedDocumentReviewCommentWatermarksRef,
1347
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef,
1348
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef,
1349
+ workspaceLastPushedTaskEventWatermarksRef,
1350
+ bootstrapPhaseStartedAtRef,
1351
+ bootstrapLastProgressAtRef,
1942
1352
  acquireWorkspaceSyncLease,
1353
+ releaseWorkspaceSyncLease,
1354
+ forceClearWorkspaceSyncLease,
1943
1355
  clearWorkspaceRetry,
1944
1356
  clearWorkspaceIssue,
1357
+ reportWorkspaceWindowContention,
1358
+ persistWorkspaceSyncPatch,
1359
+ buildWorkspaceSyncSignature,
1360
+ pullWorkspaceChangesFromCloud,
1361
+ pushWorkspaceChangesToCloud,
1362
+ cloudAuthConfigured,
1363
+ runtimeMode,
1364
+ isAuthenticated,
1365
+ checkAuthSession
1366
+ }), [
1945
1367
  currentWorkspaceId,
1946
- forceClearWorkspaceSyncLease,
1368
+ taxonomyStateFingerprint,
1947
1369
  workspaceCloudSyncEnabled,
1948
1370
  workspaceSyncPhase,
1371
+ workspaceLastPullAt,
1372
+ acquireWorkspaceSyncLease,
1373
+ releaseWorkspaceSyncLease,
1374
+ forceClearWorkspaceSyncLease,
1375
+ clearWorkspaceRetry,
1376
+ clearWorkspaceIssue,
1377
+ reportWorkspaceWindowContention,
1949
1378
  persistWorkspaceSyncPatch,
1950
1379
  buildWorkspaceSyncSignature,
1951
- workspaceLastPullAt,
1952
1380
  pullWorkspaceChangesFromCloud,
1953
1381
  pushWorkspaceChangesToCloud,
1954
- releaseWorkspaceSyncLease,
1955
- reportWorkspaceWindowContention
1382
+ cloudAuthConfigured,
1383
+ runtimeMode,
1384
+ isAuthenticated,
1385
+ checkAuthSession
1956
1386
  ]);
1387
+ const { retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull } = recoveryCoordinator;
1957
1388
  // === SECTION: Diagnostics & reactive effects ===
1958
1389
  const getWorkspaceSyncDiagnostics = useCallback(() => {
1959
1390
  return {
@@ -2012,6 +1443,13 @@ export function useSyncOrchestrator(input) {
2012
1443
  useEffect(() => {
2013
1444
  retryWorkspaceSyncRef.current = retryWorkspaceCloudSync;
2014
1445
  }, [retryWorkspaceCloudSync]);
1446
+ const refreshWorkspaceSyncContextAssetSnapshots = useCallback(() => {
1447
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1448
+ void refreshWorkspaceSyncAssetsSnapshot();
1449
+ }, [
1450
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1451
+ refreshWorkspaceSyncAssetsSnapshot
1452
+ ]);
2015
1453
  useEffect(() => {
2016
1454
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
2017
1455
  return;
@@ -2027,11 +1465,7 @@ export function useSyncOrchestrator(input) {
2027
1465
  const eventWorkspaceId = String(detail?.workspaceId || '').trim() || 'default';
2028
1466
  if (eventWorkspaceId !== currentWorkspaceId)
2029
1467
  return;
2030
- void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2031
- void refreshWorkspaceSyncAiProfilesSnapshot();
2032
- void refreshWorkspaceSyncAssetsSnapshot();
2033
- void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2034
- void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
1468
+ refreshWorkspaceSyncContextAssetSnapshots();
2035
1469
  };
2036
1470
  window.addEventListener(TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT, handleContextAssetsMutated);
2037
1471
  const docsIntervalId = window.setInterval(() => {
@@ -2064,6 +1498,7 @@ export function useSyncOrchestrator(input) {
2064
1498
  authSessionResolved,
2065
1499
  isAuthenticated,
2066
1500
  currentWorkspaceId,
1501
+ refreshWorkspaceSyncContextAssetSnapshots,
2067
1502
  refreshWorkspaceSyncMarkdownDocumentsSnapshot,
2068
1503
  refreshWorkspaceSyncAiProfilesSnapshot,
2069
1504
  refreshWorkspaceSyncAssetsSnapshot,
@@ -2082,12 +1517,8 @@ export function useSyncOrchestrator(input) {
2082
1517
  return;
2083
1518
  if (!taskAttachmentsFingerprint)
2084
1519
  return;
2085
- void refreshWorkspaceSyncAssetsSnapshot();
2086
- void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2087
- void refreshWorkspaceSyncAiProfilesSnapshot();
2088
- void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2089
- void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
2090
- }, [cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, authSessionResolved, isAuthenticated, taskAttachmentsFingerprint, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot]);
1520
+ refreshWorkspaceSyncContextAssetSnapshots();
1521
+ }, [cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, authSessionResolved, isAuthenticated, taskAttachmentsFingerprint, refreshWorkspaceSyncContextAssetSnapshots]);
2091
1522
  useEffect(() => {
2092
1523
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
2093
1524
  return;
@@ -2120,6 +1551,7 @@ export function useSyncOrchestrator(input) {
2120
1551
  buildWorkspaceSyncSignature,
2121
1552
  shouldSuppressAttachBootstrapPush,
2122
1553
  buildWorkspaceSyncPayload,
1554
+ workspaceCoreSyncFingerprint,
2123
1555
  workspaceSyncAiProfilesFingerprint,
2124
1556
  workspaceSyncMarkdownDocumentsFingerprint,
2125
1557
  workspaceSyncAssetsFingerprint,
@@ -2162,6 +1594,29 @@ export function useSyncOrchestrator(input) {
2162
1594
  pullWorkspaceChangesFromCloud,
2163
1595
  workspacePullIntervalMs
2164
1596
  ]);
1597
+ useEffect(() => {
1598
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved || !isAuthenticated)
1599
+ return;
1600
+ if (workspaceSyncPhase !== 'active')
1601
+ return;
1602
+ const intervalId = window.setInterval(() => {
1603
+ if (workspaceFullReconcileInFlightRef.current)
1604
+ return;
1605
+ workspaceFullReconcileInFlightRef.current = true;
1606
+ void pullWorkspaceChangesFromCloud({ forceCursorNull: true }).finally(() => {
1607
+ workspaceFullReconcileInFlightRef.current = false;
1608
+ });
1609
+ }, WORKSPACE_FULL_RECONCILE_INTERVAL_MS);
1610
+ return () => window.clearInterval(intervalId);
1611
+ }, [
1612
+ cloudAuthConfigured,
1613
+ runtimeMode,
1614
+ workspaceCloudSyncEnabled,
1615
+ authSessionResolved,
1616
+ isAuthenticated,
1617
+ workspaceSyncPhase,
1618
+ pullWorkspaceChangesFromCloud
1619
+ ]);
2165
1620
  useEffect(() => {
2166
1621
  if (runtimeMode !== 'local' || !isAuthenticated)
2167
1622
  return;