@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -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,8 +84,9 @@ 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
- 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;
89
+ const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, authSessionResolved, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, deletedTasks = [], 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]);
247
91
  // === SECTION: State initialization ===
248
92
  const [userGlobalSyncStatus, setUserGlobalSyncStatus] = useState('disconnected');
@@ -253,6 +97,7 @@ export function useSyncOrchestrator(input) {
253
97
  const [workspaceLastErrorMessage, setWorkspaceLastErrorMessage] = useState(null);
254
98
  const [workspaceCloudSyncEnabled, setWorkspaceCloudSyncEnabled] = useState(false);
255
99
  const [workspaceSyncPhase, setWorkspaceSyncPhase] = useState('idle');
100
+ const [workspaceSyncSetupIntent, setWorkspaceSyncSetupIntent] = useState(null);
256
101
  const [workspaceSyncBusy, setWorkspaceSyncBusy] = useState(false);
257
102
  const [workspaceSyncPendingChanges, setWorkspaceSyncPendingChanges] = useState(0);
258
103
  const [workspaceSyncMarkdownDocuments, setWorkspaceSyncMarkdownDocuments] = useState([]);
@@ -381,8 +226,12 @@ export function useSyncOrchestrator(input) {
381
226
  workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
382
227
  workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
383
228
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
229
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map();
384
230
  workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
385
231
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
232
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map();
233
+ workspaceStartupFullReconcileRanRef.current = false;
234
+ workspaceFullReconcileInFlightRef.current = false;
386
235
  const persisted = readPersistedSyncWatermarks(currentWorkspaceId, taxonomyStateFingerprint);
387
236
  if (persisted) {
388
237
  workspaceLastPushedWatermarksRef.current = persisted.taskWatermarks;
@@ -393,6 +242,7 @@ export function useSyncOrchestrator(input) {
393
242
  workspaceLastPushedDocumentReviewSessionWatermarksRef.current = persisted.documentReviewSessionWatermarks;
394
243
  workspaceLastPushedDocumentReviewCommentWatermarksRef.current = persisted.documentReviewCommentWatermarks;
395
244
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = persisted.annotatedAttachmentSessionWatermarks;
245
+ workspaceLastPushedTaskEventWatermarksRef.current = persisted.taskEventWatermarks;
396
246
  }
397
247
  }, [currentWorkspaceId, taxonomyStateFingerprint]);
398
248
  const markWorkspaceSyncReferenceSnapshotReady = useCallback((key) => {
@@ -404,14 +254,25 @@ export function useSyncOrchestrator(input) {
404
254
  }));
405
255
  }, []);
406
256
  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({
257
+ const { workspaceRetryAt, isWorkspaceRetryPending, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, scheduleWorkspaceRetry, persistWorkspaceSyncPatch, applyWorkspaceSyncPatchLocally, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot: applyWorkspaceSyncStateSnapshotBase, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease } = useWorkspaceSyncController({
408
258
  currentWorkspaceId,
409
259
  setWorkspaceCloudSyncEnabled,
410
260
  setWorkspaceSyncPhase,
261
+ setWorkspaceSyncSetupIntent,
411
262
  setWorkspaceLastPullAt,
412
263
  setWorkspaceLastPushAt,
413
264
  setWorkspaceLastErrorMessage
414
265
  });
266
+ const applyWorkspaceSyncStateSnapshot = useCallback((nextState) => {
267
+ if (nextState.phase === 'attach-cloud' || nextState.phase === 'provision-local') {
268
+ lastBootstrapPhaseRef.current = nextState.phase;
269
+ if (bootstrapPhaseStartedAtRef.current === null) {
270
+ bootstrapPhaseStartedAtRef.current = Date.now();
271
+ }
272
+ bootstrapLastProgressAtRef.current = null;
273
+ }
274
+ applyWorkspaceSyncStateSnapshotBase(nextState);
275
+ }, [applyWorkspaceSyncStateSnapshotBase]);
415
276
  const workspaceLastPushedWatermarksRef = useRef(new Map());
416
277
  const workspaceLastPushedInitiativeIdsRef = useRef(new Set());
417
278
  const workspaceLastPushedInitiativeWatermarksRef = useRef(new Map());
@@ -429,12 +290,15 @@ export function useSyncOrchestrator(input) {
429
290
  const workspaceLastPushedDocumentReviewCommentWatermarksRef = useRef(new Map());
430
291
  const workspaceLastPushedAnnotatedAttachmentSessionIdsRef = useRef(new Set());
431
292
  const workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef = useRef(new Map());
293
+ const workspaceLastPushedTaskEventWatermarksRef = useRef(new Map());
432
294
  const workspaceStartupRepairRanRef = useRef(false);
295
+ const workspaceStartupFullReconcileRanRef = useRef(false);
433
296
  const workspaceRepairModeRef = useRef(false);
434
297
  const workspaceAttachBootstrapBaselineSignatureRef = useRef('');
435
298
  const workspaceCaptureAttachBootstrapBaselineRef = useRef(false);
436
299
  const workspaceDocumentsRefreshIntervalMs = 30_000;
437
300
  const workspacePullIntervalMs = realtimeSyncEnabled ? 30_000 : 15_000;
301
+ const workspacePushWatchdogIntervalMs = Math.max(30_000, workspacePullIntervalMs);
438
302
  const workspaceLastSuccessfulSyncAt = useMemo(() => {
439
303
  const pullMs = workspaceLastPullAt ? Date.parse(workspaceLastPullAt) : NaN;
440
304
  const pushMs = workspaceLastPushAt ? Date.parse(workspaceLastPushAt) : NaN;
@@ -656,51 +520,44 @@ export function useSyncOrchestrator(input) {
656
520
  operation
657
521
  });
658
522
  }, [currentWorkspaceId]);
523
+ const persistWorkspaceSyncPatchBestEffort = useCallback(async (patch) => {
524
+ try {
525
+ return await persistWorkspaceSyncPatch(patch);
526
+ }
527
+ catch {
528
+ return applyWorkspaceSyncPatchLocally(patch);
529
+ }
530
+ }, [applyWorkspaceSyncPatchLocally, persistWorkspaceSyncPatch]);
659
531
  const persistWorkspaceSyncPatchSafely = useCallback((patch) => {
660
- void persistWorkspaceSyncPatch(patch).catch(() => { });
661
- }, [persistWorkspaceSyncPatch]);
532
+ void persistWorkspaceSyncPatchBestEffort(patch).catch(() => { });
533
+ }, [persistWorkspaceSyncPatchBestEffort]);
662
534
  const scheduleWorkspaceSyncRetry = useCallback((minDelayMs) => {
663
535
  scheduleWorkspaceRetry(() => retryWorkspaceSyncRef.current(), { minDelayMs });
664
536
  }, [scheduleWorkspaceRetry]);
665
537
  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;
538
+ syncBootstrapPhaseTracking({
539
+ workspaceSyncPhase,
540
+ lastBootstrapPhaseRef,
541
+ bootstrapPhaseStartedAtRef,
542
+ bootstrapLastProgressAtRef
543
+ });
676
544
  }, [workspaceSyncPhase]);
677
545
  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);
546
+ return createBootstrapTimeoutMonitor({
547
+ currentWorkspaceId,
548
+ workspaceCloudSyncEnabled,
549
+ workspaceSyncPhase,
550
+ workspacePullInFlightRef,
551
+ workspacePushInFlightRef,
552
+ bootstrapPhaseStartedAtRef,
553
+ bootstrapLastProgressAtRef,
554
+ setWorkspaceLastErrorMessage: (value) => setWorkspaceLastErrorMessage(value),
555
+ setWorkspaceLastErrorAt: (value) => setWorkspaceLastErrorAt(value),
556
+ setUserGlobalSyncStatus,
557
+ setUserGlobalSyncError: (value) => setUserGlobalSyncError(value),
558
+ setWorkspaceSyncBusy,
559
+ persistWorkspaceSyncPatchSafely
560
+ });
704
561
  }, [
705
562
  currentWorkspaceId,
706
563
  workspaceCloudSyncEnabled,
@@ -716,7 +573,8 @@ export function useSyncOrchestrator(input) {
716
573
  isAuthenticated,
717
574
  resolveCloudAuthUrl,
718
575
  workspaceId: currentWorkspaceId,
719
- workspaceName: projectName || currentWorkspaceId
576
+ workspaceName: projectName || currentWorkspaceId,
577
+ allowProvisionOnMismatch: workspaceSyncPhase !== 'attach-cloud'
720
578
  });
721
579
  }, [
722
580
  cloudAuthConfigured,
@@ -724,7 +582,8 @@ export function useSyncOrchestrator(input) {
724
582
  isAuthenticated,
725
583
  resolveCloudAuthUrl,
726
584
  currentWorkspaceId,
727
- projectName
585
+ projectName,
586
+ workspaceSyncPhase
728
587
  ]);
729
588
  useEffect(() => {
730
589
  workspaceTasksRef.current = tasks || [];
@@ -732,6 +591,14 @@ export function useSyncOrchestrator(input) {
732
591
  useEffect(() => {
733
592
  workspaceArchivedTasksRef.current = archivedTasks || [];
734
593
  }, [archivedTasks]);
594
+ useEffect(() => {
595
+ workspaceDeletedTaskWatermarksRef.current = new Map((deletedTasks || [])
596
+ .map((entry) => [
597
+ String(entry?.taskId || '').trim(),
598
+ String(entry?.deletedAt || '').trim()
599
+ ])
600
+ .filter(([taskId, deletedAt]) => taskId.length > 0 && Number.isFinite(Date.parse(deletedAt))));
601
+ }, [deletedTasks, workspaceDeletedTaskWatermarksRef]);
735
602
  useEffect(() => {
736
603
  workspaceInitiativesRef.current = initiatives || [];
737
604
  }, [initiatives]);
@@ -739,6 +606,7 @@ export function useSyncOrchestrator(input) {
739
606
  workspaceWorkstreamsRef.current = workstreams || [];
740
607
  }, [workstreams]);
741
608
  const workspaceForceFullAiProfilePushRef = useRef(false);
609
+ const workspaceFullReconcileInFlightRef = useRef(false);
742
610
  const buildWorkspaceSyncPayload = useCallback((options) => {
743
611
  const forceAllAiProfiles = options?.forceAllAiProfiles === true;
744
612
  // Read documents and assets from refs rather than state so that the freshest data
@@ -747,8 +615,10 @@ export function useSyncOrchestrator(input) {
747
615
  return buildWorkspaceSyncPayloadModel({
748
616
  tasks: workspaceTasksRef.current,
749
617
  archivedTasks: workspaceArchivedTasksRef.current,
618
+ taskEvents: flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current),
750
619
  lastPushedTaskIds: workspaceLastPushedTaskIdsRef.current,
751
620
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
621
+ pendingDeletedTaskWatermarks: workspaceDeletedTaskWatermarksRef.current,
752
622
  initiatives: workspaceInitiativesRef.current,
753
623
  workstreams: workspaceWorkstreamsRef.current,
754
624
  taxonomies: taxonomies || [],
@@ -775,6 +645,7 @@ export function useSyncOrchestrator(input) {
775
645
  lastPushedDocumentReviewCommentWatermarks: workspaceLastPushedDocumentReviewCommentWatermarksRef.current,
776
646
  lastPushedAnnotatedAttachmentSessionIds: workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current,
777
647
  lastPushedAnnotatedAttachmentSessionWatermarks: workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current,
648
+ lastPushedTaskEventWatermarks: workspaceLastPushedTaskEventWatermarksRef.current,
778
649
  lastPushedWatermarks: workspaceLastPushedWatermarksRef.current
779
650
  });
780
651
  }, [taxonomies, taxonomyState]);
@@ -802,6 +673,7 @@ export function useSyncOrchestrator(input) {
802
673
  workspaceId: currentWorkspaceId,
803
674
  tasks: workspaceTasksRef.current,
804
675
  archivedTasks: workspaceArchivedTasksRef.current,
676
+ taskEvents: flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current),
805
677
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
806
678
  initiatives: workspaceInitiativesRef.current,
807
679
  workstreams: workspaceWorkstreamsRef.current,
@@ -813,7 +685,52 @@ export function useSyncOrchestrator(input) {
813
685
  documentReviewComments: workspaceSyncDocumentReviewCommentsRef.current,
814
686
  annotatedAttachmentSessions: workspaceSyncAnnotatedAttachmentSessionsRef.current
815
687
  });
816
- }, [currentWorkspaceId, taxonomies]);
688
+ }, [currentWorkspaceId, taxonomies, taxonomyState]);
689
+ const workspaceCoreSyncFingerprint = useMemo(() => JSON.stringify({
690
+ tasks: (tasks || [])
691
+ .map((task) => ({
692
+ id: String(task.id || '').trim(),
693
+ updatedAt: String(task.updatedAt || task.createdAt || '').trim(),
694
+ status: String(task.status || '').trim(),
695
+ referenceNumber: task.referenceNumber ?? null,
696
+ localReferenceNumber: task.localReferenceNumber ?? null
697
+ }))
698
+ .sort((left, right) => left.id.localeCompare(right.id)),
699
+ archivedTasks: (archivedTasks || [])
700
+ .map((task) => ({
701
+ id: String(task.id || '').trim(),
702
+ updatedAt: String(task.updatedAt || task.createdAt || '').trim(),
703
+ status: String(task.status || '').trim(),
704
+ referenceNumber: task.referenceNumber ?? null,
705
+ localReferenceNumber: task.localReferenceNumber ?? null
706
+ }))
707
+ .sort((left, right) => left.id.localeCompare(right.id)),
708
+ initiatives: (initiatives || [])
709
+ .map((initiative) => ({
710
+ id: String(initiative.id || '').trim(),
711
+ updatedAt: String(initiative.updatedAt || initiative.createdAt || '').trim(),
712
+ isArchived: Boolean(initiative.isArchived)
713
+ }))
714
+ .sort((left, right) => left.id.localeCompare(right.id)),
715
+ workstreams: (workstreams || [])
716
+ .map((workstream) => ({
717
+ id: String(workstream.id || '').trim(),
718
+ updatedAt: String(workstream.updatedAt || workstream.createdAt || '').trim(),
719
+ initiativeId: typeof workstream.initiativeId === 'string' ? workstream.initiativeId.trim() : '',
720
+ isArchived: Boolean(workstream.isArchived)
721
+ }))
722
+ .sort((left, right) => left.id.localeCompare(right.id)),
723
+ taskEvents: flattenTaskEvents(tasks || [], archivedTasks || [])
724
+ .map((event) => ({
725
+ id: String(event.id || '').trim(),
726
+ taskId: String(event.taskId || '').trim(),
727
+ createdAt: String(event.createdAt || '').trim(),
728
+ action: String(event.action || '').trim(),
729
+ }))
730
+ .sort((left, right) => left.id.localeCompare(right.id)),
731
+ taxonomies: Array.isArray(taxonomies) ? taxonomies : [],
732
+ taxonomyState: taxonomyState && typeof taxonomyState === 'object' ? taxonomyState : null
733
+ }), [tasks, archivedTasks, initiatives, workstreams, taxonomies, taxonomyState]);
817
734
  // Fingerprint of per-task attachment counts — changes when the MCP tool saves a new
818
735
  // attachment and the React app polls the updated task. Used to trigger an immediate
819
736
  // asset snapshot refresh without requiring a cross-process DOM event.
@@ -1172,6 +1089,9 @@ export function useSyncOrchestrator(input) {
1172
1089
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map(workspaceSyncAnnotatedAttachmentSessionsRef.current
1173
1090
  .map((entry) => [String(entry?.id || '').trim(), String(entry?.updatedAt || entry?.createdAt || '').trim()])
1174
1091
  .filter(([id]) => id.length > 0));
1092
+ workspaceLastPushedTaskEventWatermarksRef.current = new Map(flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current)
1093
+ .map((entry) => [String(entry?.id || '').trim(), String(entry?.createdAt || '').trim()])
1094
+ .filter(([id]) => id.length > 0));
1175
1095
  }, []);
1176
1096
  const refreshLocalWorkspaceTaskCollections = useCallback(async () => {
1177
1097
  const [tasksRes, archiveRes] = await Promise.all([
@@ -1208,557 +1128,174 @@ export function useSyncOrchestrator(input) {
1208
1128
  });
1209
1129
  }, [currentWorkspaceId, fetchPlanningEntities, setArchivedTasks, setTasks]);
1210
1130
  // === 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
- }, [
1131
+ const { handleSyncAuthFailure, ensureWorkspaceSyncReady } = useMemo(() => createSyncAuthCoordinator({
1132
+ currentWorkspaceId,
1133
+ workspaceSyncPhase,
1229
1134
  clearWorkspaceRetry,
1135
+ setWorkspaceSyncBusy,
1136
+ setUserGlobalSyncStatus,
1137
+ setUserGlobalSyncError: (value) => setUserGlobalSyncError(value),
1138
+ setWorkspaceLastErrorMessage: (value) => setWorkspaceLastErrorMessage(value),
1139
+ setWorkspaceLastErrorAt: (value) => setWorkspaceLastErrorAt(value),
1230
1140
  reportSyncEvent,
1231
- currentWorkspaceId,
1232
1141
  setAuthBlocked,
1233
1142
  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,
1143
+ checkAuthSession,
1272
1144
  ensureCloudWorkspaceReadyForSync,
1273
- handleSyncAuthFailure,
1274
- reportSyncEvent,
1275
1145
  scheduleWorkspaceSyncRetry,
1146
+ persistWorkspaceSyncPatchSafely
1147
+ }), [
1148
+ currentWorkspaceId,
1276
1149
  workspaceSyncPhase,
1150
+ clearWorkspaceRetry,
1151
+ reportSyncEvent,
1152
+ setAuthBlocked,
1153
+ setIsAuthenticated,
1154
+ checkAuthSession,
1155
+ ensureCloudWorkspaceReadyForSync,
1156
+ scheduleWorkspaceSyncRetry,
1277
1157
  persistWorkspaceSyncPatchSafely
1278
1158
  ]);
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
- }, [
1159
+ // === SECTION: Push/Pull sync ===
1160
+ const transferCoordinator = useMemo(() => createWorkspaceTransferCoordinator({
1475
1161
  cloudAuthConfigured,
1476
1162
  runtimeMode,
1477
1163
  workspaceCloudSyncEnabled,
1478
1164
  currentWorkspaceId,
1479
1165
  workspaceSyncPhase,
1166
+ taxonomyStateFingerprint,
1480
1167
  isAuthenticated,
1481
1168
  checkAuthSession,
1482
- clearWorkspaceIssue,
1483
- refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1484
- refreshWorkspaceSyncAiProfilesSnapshot,
1485
- refreshWorkspaceSyncAssetsSnapshot,
1486
- refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1487
- ensureWorkspaceSyncReady,
1488
- buildWorkspaceSyncPayload,
1489
1169
  resolveCloudAuthUrl,
1490
1170
  ensureCloudWorkspaceReadyForSync,
1491
- shouldSuppressAttachBootstrapPush,
1171
+ ensureWorkspaceSyncReady,
1492
1172
  handleSyncAuthFailure,
1493
1173
  clearWorkspaceRetry,
1494
- persistWorkspaceSyncPatch,
1495
- reportSyncEvent,
1174
+ clearWorkspaceIssue,
1175
+ isWorkspaceRetryPending,
1496
1176
  scheduleWorkspaceSyncRetry,
1177
+ persistWorkspaceSyncPatchSafely,
1178
+ persistWorkspaceSyncPatchBestEffort,
1179
+ reportSyncEvent,
1497
1180
  acquireWorkspaceSyncLease,
1498
1181
  releaseWorkspaceSyncLease,
1499
1182
  reportWorkspaceWindowContention,
1183
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1184
+ refreshWorkspaceSyncAiProfilesSnapshot,
1185
+ refreshWorkspaceSyncAssetsSnapshot,
1186
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1187
+ refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
1188
+ refreshLocalWorkspaceTaskCollections,
1189
+ buildWorkspaceSyncPayload,
1190
+ buildWorkspaceSyncSignature,
1191
+ seedWorkspaceSnapshotPushBaseline,
1192
+ shouldSuppressAttachBootstrapPush,
1193
+ recordSuppressedEventIds: (...args) => realtimeBridgeRef.current.recordSuppressedEventIds(...args),
1194
+ workspaceRepairModeRef,
1195
+ workspaceForceFullAiProfilePushRef,
1500
1196
  workspacePushInFlightRef,
1501
1197
  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
- details: {
1576
- pages: pullResult.pages,
1577
- applyMs: pullResult.applyMs,
1578
- ...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
1579
- }
1580
- });
1581
- if (pullResult.transient || pullResult.hasTransientApplyFailure || isTransientWorkspaceSyncStatus(pullResult.status)) {
1582
- scheduleWorkspaceSyncRetry(pullResult.retryAfterMs);
1583
- }
1584
- else {
1585
- persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
1586
- }
1587
- return false;
1588
- }
1589
- if (Array.isArray(pullResult.emittedEventIds)) {
1590
- for (const eventId of pullResult.emittedEventIds) {
1591
- if (eventId) {
1592
- realtimeSuppressedEventIdsRef.current.add(eventId);
1593
- // Optional: cleanup after a generous delay if WebSocket never arrives to clear it
1594
- setTimeout(() => {
1595
- realtimeSuppressedEventIdsRef.current.delete(eventId);
1596
- }, 60000);
1597
- }
1598
- }
1599
- }
1600
- if (pullResult.pulledDeleteTaskIds.size > 0) {
1601
- setTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
1602
- setArchivedTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
1603
- }
1604
- const decryptFailures = Array.isArray(pullResult.documentDecryptFailures)
1605
- ? pullResult.documentDecryptFailures.filter((value) => String(value || '').trim().length > 0)
1606
- : [];
1607
- if (decryptFailures.length > 0) {
1608
- logSyncDebug('pull_document_decrypt_failures', {
1609
- workspaceId: currentWorkspaceId,
1610
- failureCount: decryptFailures.length
1611
- });
1612
- }
1613
- workspacePullCursorRef.current = pullResult.cursor || null;
1614
- if (pullResult.appliedChanges > 0 && (pullResult.pulledActiveUpserts || pullResult.pulledArchivedUpserts)) {
1615
- try {
1616
- await refreshLocalWorkspaceTaskCollections();
1617
- }
1618
- catch {
1619
- logSyncDebug('pull_local_refresh_failed', {
1620
- workspaceId: currentWorkspaceId,
1621
- appliedChanges: pullResult.appliedChanges
1622
- });
1623
- }
1624
- }
1625
- clearWorkspaceRetry();
1626
- clearWorkspaceIssue();
1627
- const syncedAt = pullResult.syncedAt || new Date().toISOString();
1628
- setWorkspaceLastPullAt(syncedAt);
1629
- setWorkspaceSyncPendingChanges(0);
1630
- setUserGlobalSyncStatus('idle');
1631
- if (workspaceSyncPhase === 'attach-cloud') {
1632
- workspaceCaptureAttachBootstrapBaselineRef.current = true;
1633
- workspacePendingSignatureRef.current = '';
1634
- }
1635
- await persistWorkspaceSyncPatch({
1636
- phase: 'active',
1637
- pullCursor: workspacePullCursorRef.current,
1638
- lastPullAt: syncedAt,
1639
- lastSyncedAt: syncedAt,
1640
- lastErrorMessage: null
1641
- });
1642
- reportSyncEvent(currentWorkspaceId, {
1643
- eventType: workspaceSyncPhase === 'attach-cloud' ? 'bootstrap' : 'pull',
1644
- status: 'success',
1645
- changeCount: pullResult.appliedChanges,
1646
- requestMs: pullResult.pullRequestMs,
1647
- details: {
1648
- pages: pullResult.pages,
1649
- applyMs: pullResult.applyMs,
1650
- ...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
1651
- }
1652
- });
1653
- if (workspaceSyncPhase === 'attach-cloud') {
1654
- await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1655
- await refreshWorkspaceSyncAssetsSnapshot();
1656
- await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
1657
- await refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
1658
- seedWorkspaceSnapshotPushBaseline();
1659
- return true;
1660
- }
1661
- const pendingSignature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
1662
- if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
1663
- workspacePendingSignatureRef.current = '';
1664
- window.setTimeout(() => {
1665
- void pushWorkspaceChangesToCloud(pendingSignature);
1666
- }, 120);
1667
- }
1668
- return true;
1669
- }
1670
- catch (error) {
1671
- const detail = String(error?.message || '').trim();
1672
- const message = detail
1673
- ? `Workspace sync pull failed. ${detail}`
1674
- : 'Workspace sync pull failed.';
1675
- setUserGlobalSyncStatus('error');
1676
- setUserGlobalSyncError(message);
1677
- setWorkspaceLastErrorMessage(message);
1678
- setWorkspaceLastErrorAt(new Date().toISOString());
1679
- scheduleWorkspaceSyncRetry();
1680
- reportSyncEvent(currentWorkspaceId, {
1681
- eventType: 'pull',
1682
- status: 'error',
1683
- errorMessage: message,
1684
- requestMs: Date.now() - pullStartedAtMs
1685
- });
1686
- logSyncDebug('pull_failed_exception', {
1687
- workspaceId: currentWorkspaceId,
1688
- elapsedMs: Date.now() - pullStartedAtMs,
1689
- error: detail || null
1690
- });
1691
- return false;
1692
- }
1693
- finally {
1694
- workspacePullInFlightRef.current = false;
1695
- setWorkspaceSyncBusy(workspacePushInFlightRef.current);
1696
- releaseWorkspaceSyncLease(currentWorkspaceId);
1697
- }
1698
- }, [
1198
+ workspaceLastPushedSignatureRef,
1199
+ workspacePendingSignatureRef,
1200
+ workspaceLastPushedTaskIdsRef,
1201
+ workspaceDeletedTaskIdsRef,
1202
+ workspaceDeletedTaskWatermarksRef,
1203
+ workspacePullCursorRef,
1204
+ workspaceLastPushedWatermarksRef,
1205
+ workspaceLastPushedInitiativeIdsRef,
1206
+ workspaceLastPushedInitiativeWatermarksRef,
1207
+ workspaceLastPushedWorkstreamIdsRef,
1208
+ workspaceLastPushedWorkstreamWatermarksRef,
1209
+ workspaceLastPushedAiProfileIdsRef,
1210
+ workspaceLastPushedAiProfileWatermarksRef,
1211
+ workspaceLastPushedDocumentPathsRef,
1212
+ workspaceLastPushedDocumentWatermarksRef,
1213
+ workspaceLastPushedAssetPathsRef,
1214
+ workspaceLastPushedAssetWatermarksRef,
1215
+ workspaceLastPushedDocumentReviewSessionIdsRef,
1216
+ workspaceLastPushedDocumentReviewSessionWatermarksRef,
1217
+ workspaceLastPushedDocumentReviewCommentIdsRef,
1218
+ workspaceLastPushedDocumentReviewCommentWatermarksRef,
1219
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef,
1220
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef,
1221
+ workspaceLastPushedTaskEventWatermarksRef,
1222
+ workspaceCaptureAttachBootstrapBaselineRef,
1223
+ workspaceStartupFullReconcileRanRef,
1224
+ workspaceFullReconcileInFlightRef,
1225
+ bootstrapLastProgressAtRef,
1226
+ realtimeSuppressedEventQueueRef,
1227
+ setWorkspaceSyncBusy,
1228
+ setUserGlobalSyncStatus,
1229
+ setUserGlobalSyncError,
1230
+ setWorkspaceLastErrorMessage,
1231
+ setWorkspaceLastErrorAt,
1232
+ setWorkspaceLastPullAt,
1233
+ setWorkspaceLastPushAt,
1234
+ setWorkspaceSyncPendingChanges,
1235
+ setTasks,
1236
+ setArchivedTasks
1237
+ }), [
1699
1238
  cloudAuthConfigured,
1700
1239
  runtimeMode,
1701
1240
  workspaceCloudSyncEnabled,
1702
1241
  currentWorkspaceId,
1703
1242
  workspaceSyncPhase,
1704
- isWorkspaceRetryPending,
1243
+ taxonomyStateFingerprint,
1705
1244
  isAuthenticated,
1706
1245
  checkAuthSession,
1707
- clearWorkspaceIssue,
1708
- ensureWorkspaceSyncReady,
1709
1246
  resolveCloudAuthUrl,
1710
1247
  ensureCloudWorkspaceReadyForSync,
1248
+ ensureWorkspaceSyncReady,
1711
1249
  handleSyncAuthFailure,
1712
- refreshLocalWorkspaceTaskCollections,
1250
+ clearWorkspaceRetry,
1251
+ clearWorkspaceIssue,
1252
+ scheduleWorkspaceSyncRetry,
1253
+ persistWorkspaceSyncPatchSafely,
1254
+ persistWorkspaceSyncPatchBestEffort,
1255
+ reportSyncEvent,
1256
+ acquireWorkspaceSyncLease,
1257
+ releaseWorkspaceSyncLease,
1258
+ reportWorkspaceWindowContention,
1713
1259
  refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1260
+ refreshWorkspaceSyncAiProfilesSnapshot,
1714
1261
  refreshWorkspaceSyncAssetsSnapshot,
1715
1262
  refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1716
1263
  refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
1717
- clearWorkspaceRetry,
1718
- persistWorkspaceSyncPatch,
1719
- reportSyncEvent,
1264
+ refreshLocalWorkspaceTaskCollections,
1265
+ buildWorkspaceSyncPayload,
1720
1266
  buildWorkspaceSyncSignature,
1721
- pushWorkspaceChangesToCloud,
1722
1267
  seedWorkspaceSnapshotPushBaseline,
1723
- scheduleWorkspaceSyncRetry,
1724
- acquireWorkspaceSyncLease,
1725
- releaseWorkspaceSyncLease,
1726
- reportWorkspaceWindowContention,
1727
- workspacePushInFlightRef,
1728
- workspacePullInFlightRef,
1729
- persistWorkspaceSyncPatchSafely
1268
+ shouldSuppressAttachBootstrapPush,
1269
+ setTasks,
1270
+ setArchivedTasks
1730
1271
  ]);
1731
- // === SECTION: Realtime sync integration ===
1732
- const handleRealtimeSignal = useCallback((signal) => {
1733
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1734
- return;
1735
- if (workspaceSyncPhase === 'provision-local')
1736
- return;
1737
- const eventId = String(signal?.eventId || '').trim();
1738
- if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
1739
- return;
1740
- if (workspacePullInFlightRef.current)
1741
- return;
1742
- if (isWorkspaceRetryPending()) {
1743
- clearWorkspaceRetry();
1744
- }
1745
- if (realtimePullDebounceRef.current !== null) {
1746
- window.clearTimeout(realtimePullDebounceRef.current);
1747
- }
1748
- realtimePullDebounceRef.current = window.setTimeout(() => {
1749
- realtimePullDebounceRef.current = null;
1750
- void pullWorkspaceChangesFromCloud();
1751
- }, 180);
1752
- }, [
1272
+ const { pushWorkspaceChangesToCloud, pullWorkspaceChangesFromCloud } = transferCoordinator;
1273
+ const realtimeBridgeRef = useRef({
1274
+ handleRealtimeSignal: () => { },
1275
+ recordSuppressedEventIds: () => { },
1276
+ clearRealtimePullDebounce: () => { }
1277
+ });
1278
+ const realtimeBridge = useMemo(() => createRealtimeSyncBridge({
1753
1279
  cloudAuthConfigured,
1754
1280
  runtimeMode,
1755
1281
  workspaceCloudSyncEnabled,
1756
1282
  workspaceSyncPhase,
1283
+ workspacePullInFlightRef,
1284
+ realtimePullDebounceRef,
1285
+ realtimeSuppressedEventIdsRef,
1286
+ clearWorkspaceRetry,
1757
1287
  isWorkspaceRetryPending,
1288
+ pullWorkspaceChangesFromCloud
1289
+ }), [
1290
+ cloudAuthConfigured,
1291
+ runtimeMode,
1292
+ workspaceCloudSyncEnabled,
1293
+ workspaceSyncPhase,
1758
1294
  clearWorkspaceRetry,
1759
- workspacePullInFlightRef,
1295
+ isWorkspaceRetryPending,
1760
1296
  pullWorkspaceChangesFromCloud
1761
1297
  ]);
1298
+ realtimeBridgeRef.current = realtimeBridge;
1762
1299
  const realtimeSocketUrl = resolveWebSocketUrl('/taskforce-ws');
1763
1300
  const normalizedRealtimeUserId = String(authUserId || '').trim();
1764
1301
  const realtimeEnabled = Boolean(realtimeSyncEnabled
@@ -1774,7 +1311,7 @@ export function useSyncOrchestrator(input) {
1774
1311
  enabled: realtimeEnabled,
1775
1312
  workspaceId: currentWorkspaceId,
1776
1313
  websocketUrl: realtimeSocketUrl,
1777
- onSignal: handleRealtimeSignal,
1314
+ onSignal: realtimeBridge.handleRealtimeSignal,
1778
1315
  onTelemetry: setRealtimeTelemetry,
1779
1316
  userId: normalizedRealtimeUserId || undefined
1780
1317
  });
@@ -1782,39 +1319,9 @@ export function useSyncOrchestrator(input) {
1782
1319
  setRealtimeConnectionState(realtimeSync.connectionState);
1783
1320
  }, [realtimeSync.connectionState]);
1784
1321
  useEffect(() => {
1785
- return () => {
1786
- if (realtimePullDebounceRef.current !== null) {
1787
- window.clearTimeout(realtimePullDebounceRef.current);
1788
- realtimePullDebounceRef.current = null;
1789
- }
1790
- };
1791
- }, []);
1322
+ return () => realtimeBridge.clearRealtimePullDebounce();
1323
+ }, [realtimeBridge]);
1792
1324
  // === SECTION: Repair & manual retry ===
1793
- const fetchLocalWorkspaceSnapshot = useCallback(async () => {
1794
- const [localTasksRes, localArchiveRes] = await Promise.all([
1795
- fetch('/api/taskforce/tasks', {
1796
- method: 'GET',
1797
- credentials: 'include'
1798
- }),
1799
- fetch('/api/taskforce/archive', {
1800
- method: 'GET',
1801
- credentials: 'include'
1802
- })
1803
- ]);
1804
- if (!localTasksRes.ok || !localArchiveRes.ok) {
1805
- return {
1806
- tasks: [],
1807
- archived: [],
1808
- error: 'Failed to read local workspace snapshot before sync bootstrap.'
1809
- };
1810
- }
1811
- const localTasksPayload = await localTasksRes.json().catch(() => ({}));
1812
- const localArchivePayload = await localArchiveRes.json().catch(() => ({}));
1813
- return {
1814
- tasks: Array.isArray(localTasksPayload?.tasks) ? localTasksPayload.tasks : [],
1815
- archived: Array.isArray(localArchivePayload?.archived) ? localArchivePayload.archived : []
1816
- };
1817
- }, []);
1818
1325
  const saveWorkspaceCloudSyncSettings = useCallback(async (settings) => {
1819
1326
  return saveWorkspaceCloudSyncSettingsBase(settings, {
1820
1327
  cloudAuthConfigured,
@@ -1830,144 +1337,79 @@ export function useSyncOrchestrator(input) {
1830
1337
  const retryUserGlobalSettingsSync = useCallback(async () => {
1831
1338
  await syncUserGlobalSettings({ preferCloudOnFirstSync: false });
1832
1339
  }, [syncUserGlobalSettings]);
1833
- const retryWorkspaceCloudSync = useCallback(async () => {
1834
- clearWorkspaceRetry();
1835
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1836
- return false;
1837
- if (!isAuthenticated) {
1838
- const authed = await checkAuthSession();
1839
- if (!authed)
1840
- return false;
1841
- }
1842
- workspaceForceFullAiProfilePushRef.current = true;
1843
- if (workspaceSyncPhase === 'provision-local') {
1844
- const signature = buildWorkspaceSyncSignature();
1845
- if (!signature)
1846
- return false;
1847
- return pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true });
1848
- }
1849
- const pulled = await pullWorkspaceChangesFromCloud();
1850
- if (workspaceSyncPhase === 'attach-cloud')
1851
- return pulled;
1852
- const signature = buildWorkspaceSyncSignature();
1853
- const forceAllAiProfiles = workspaceForceFullAiProfilePushRef.current === true;
1854
- if (!forceAllAiProfiles && shouldSuppressAttachBootstrapPush(signature)) {
1855
- return pulled;
1856
- }
1857
- if (signature) {
1858
- workspacePendingSignatureRef.current = signature;
1859
- const pushed = await pushWorkspaceChangesToCloud(signature, {
1860
- forceAllAiProfiles
1861
- });
1862
- return pulled || pushed;
1863
- }
1864
- return pulled;
1865
- }, [
1866
- clearWorkspaceRetry,
1867
- cloudAuthConfigured,
1868
- runtimeMode,
1340
+ const recoveryCoordinator = useMemo(() => createSyncRecoveryCoordinator({
1341
+ currentWorkspaceId,
1342
+ taxonomyStateFingerprint,
1869
1343
  workspaceCloudSyncEnabled,
1870
- isAuthenticated,
1871
- checkAuthSession,
1872
1344
  workspaceSyncPhase,
1873
- buildWorkspaceSyncSignature,
1874
- shouldSuppressAttachBootstrapPush,
1875
- pushWorkspaceChangesToCloud,
1876
- pullWorkspaceChangesFromCloud
1877
- ]);
1878
- const resetWorkspaceSyncCursorAndPull = useCallback(async () => {
1879
- const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
1880
- if (!leaseAcquired) {
1881
- forceClearWorkspaceSyncLease(currentWorkspaceId);
1882
- const reclaimed = await acquireWorkspaceSyncLease(currentWorkspaceId, 'repair');
1883
- if (!reclaimed) {
1884
- reportWorkspaceWindowContention('repair');
1885
- return;
1886
- }
1887
- }
1888
- clearWorkspaceRetry();
1889
- clearWorkspaceIssue();
1890
- bootstrapPhaseStartedAtRef.current = null;
1891
- bootstrapLastProgressAtRef.current = null;
1892
- workspacePullCursorRef.current = null;
1893
- workspaceAttachBootstrapBaselineSignatureRef.current = '';
1894
- workspaceCaptureAttachBootstrapBaselineRef.current = false;
1895
- workspacePendingSignatureRef.current = '';
1896
- workspaceLastPushedSignatureRef.current = '';
1897
- workspaceLastPushedWatermarksRef.current = new Map();
1898
- workspaceLastPushedInitiativeIdsRef.current = new Set();
1899
- workspaceLastPushedInitiativeWatermarksRef.current = new Map();
1900
- workspaceLastPushedWorkstreamIdsRef.current = new Set();
1901
- workspaceLastPushedWorkstreamWatermarksRef.current = new Map();
1902
- workspaceLastPushedAiProfileIdsRef.current = new Set();
1903
- workspaceLastPushedAiProfileWatermarksRef.current = new Map();
1904
- workspaceLastPushedDocumentPathsRef.current = new Set();
1905
- workspaceLastPushedDocumentWatermarksRef.current = new Map();
1906
- workspaceLastPushedAssetPathsRef.current = new Set();
1907
- workspaceLastPushedAssetWatermarksRef.current = new Map();
1908
- workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set();
1909
- workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map();
1910
- workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set();
1911
- workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
1912
- workspaceForceFullAiProfilePushRef.current = false;
1913
- workspaceStartupRepairRanRef.current = false;
1914
- // Clear persisted watermarks so the next push is a full resync (repair intent)
1915
- savePersistedSyncWatermarks(currentWorkspaceId, {
1916
- taskWatermarks: new Map(),
1917
- initiativeWatermarks: new Map(),
1918
- workstreamWatermarks: new Map(),
1919
- documentWatermarks: new Map(),
1920
- assetWatermarks: new Map(),
1921
- documentReviewSessionWatermarks: new Map(),
1922
- documentReviewCommentWatermarks: new Map(),
1923
- annotatedAttachmentSessionWatermarks: new Map(),
1924
- taxonomyStateFingerprint
1925
- });
1926
- try {
1927
- if (!workspaceCloudSyncEnabled)
1928
- return;
1929
- workspaceRepairModeRef.current = true;
1930
- const repairPhase = workspaceSyncPhase === 'provision-local'
1931
- ? 'provision-local'
1932
- : workspaceSyncPhase === 'attach-cloud'
1933
- ? 'attach-cloud'
1934
- : lastBootstrapPhaseRef.current === 'provision-local' || lastBootstrapPhaseRef.current === 'attach-cloud'
1935
- ? lastBootstrapPhaseRef.current
1936
- : (workspaceLastPullAt ? 'attach-cloud' : 'provision-local');
1937
- await persistWorkspaceSyncPatch({
1938
- phase: repairPhase,
1939
- pullCursor: null,
1940
- lastErrorMessage: null
1941
- });
1942
- if (repairPhase === 'provision-local') {
1943
- const signature = buildWorkspaceSyncSignature();
1944
- if (!signature)
1945
- return;
1946
- await pushWorkspaceChangesToCloud(signature, { forceAllAiProfiles: true, repairMode: true });
1947
- return;
1948
- }
1949
- await pullWorkspaceChangesFromCloud({ repairMode: true });
1950
- }
1951
- finally {
1952
- workspaceRepairModeRef.current = false;
1953
- releaseWorkspaceSyncLease(currentWorkspaceId);
1954
- }
1955
- }, [
1345
+ workspaceLastPullAt,
1346
+ lastBootstrapPhaseRef,
1347
+ workspaceRepairModeRef,
1348
+ workspaceForceFullAiProfilePushRef,
1349
+ workspaceStartupRepairRanRef,
1350
+ workspaceStartupFullReconcileRanRef,
1351
+ workspaceFullReconcileInFlightRef,
1352
+ workspacePullCursorRef,
1353
+ workspaceAttachBootstrapBaselineSignatureRef,
1354
+ workspaceCaptureAttachBootstrapBaselineRef,
1355
+ workspacePendingSignatureRef,
1356
+ workspaceLastPushedSignatureRef,
1357
+ workspaceLastPushedWatermarksRef,
1358
+ workspaceLastPushedInitiativeIdsRef,
1359
+ workspaceLastPushedInitiativeWatermarksRef,
1360
+ workspaceLastPushedWorkstreamIdsRef,
1361
+ workspaceLastPushedWorkstreamWatermarksRef,
1362
+ workspaceLastPushedAiProfileIdsRef,
1363
+ workspaceLastPushedAiProfileWatermarksRef,
1364
+ workspaceLastPushedDocumentPathsRef,
1365
+ workspaceLastPushedDocumentWatermarksRef,
1366
+ workspaceLastPushedAssetPathsRef,
1367
+ workspaceLastPushedAssetWatermarksRef,
1368
+ workspaceLastPushedDocumentReviewSessionIdsRef,
1369
+ workspaceLastPushedDocumentReviewSessionWatermarksRef,
1370
+ workspaceLastPushedDocumentReviewCommentIdsRef,
1371
+ workspaceLastPushedDocumentReviewCommentWatermarksRef,
1372
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef,
1373
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef,
1374
+ workspaceLastPushedTaskEventWatermarksRef,
1375
+ bootstrapPhaseStartedAtRef,
1376
+ bootstrapLastProgressAtRef,
1956
1377
  acquireWorkspaceSyncLease,
1378
+ releaseWorkspaceSyncLease,
1379
+ forceClearWorkspaceSyncLease,
1957
1380
  clearWorkspaceRetry,
1958
1381
  clearWorkspaceIssue,
1382
+ reportWorkspaceWindowContention,
1383
+ persistWorkspaceSyncPatch,
1384
+ buildWorkspaceSyncSignature,
1385
+ pullWorkspaceChangesFromCloud,
1386
+ pushWorkspaceChangesToCloud,
1387
+ cloudAuthConfigured,
1388
+ runtimeMode,
1389
+ isAuthenticated,
1390
+ checkAuthSession
1391
+ }), [
1959
1392
  currentWorkspaceId,
1960
- forceClearWorkspaceSyncLease,
1393
+ taxonomyStateFingerprint,
1961
1394
  workspaceCloudSyncEnabled,
1962
1395
  workspaceSyncPhase,
1396
+ workspaceLastPullAt,
1397
+ acquireWorkspaceSyncLease,
1398
+ releaseWorkspaceSyncLease,
1399
+ forceClearWorkspaceSyncLease,
1400
+ clearWorkspaceRetry,
1401
+ clearWorkspaceIssue,
1402
+ reportWorkspaceWindowContention,
1963
1403
  persistWorkspaceSyncPatch,
1964
1404
  buildWorkspaceSyncSignature,
1965
- workspaceLastPullAt,
1966
1405
  pullWorkspaceChangesFromCloud,
1967
1406
  pushWorkspaceChangesToCloud,
1968
- releaseWorkspaceSyncLease,
1969
- reportWorkspaceWindowContention
1407
+ cloudAuthConfigured,
1408
+ runtimeMode,
1409
+ isAuthenticated,
1410
+ checkAuthSession
1970
1411
  ]);
1412
+ const { retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull } = recoveryCoordinator;
1971
1413
  // === SECTION: Diagnostics & reactive effects ===
1972
1414
  const getWorkspaceSyncDiagnostics = useCallback(() => {
1973
1415
  return {
@@ -2026,6 +1468,13 @@ export function useSyncOrchestrator(input) {
2026
1468
  useEffect(() => {
2027
1469
  retryWorkspaceSyncRef.current = retryWorkspaceCloudSync;
2028
1470
  }, [retryWorkspaceCloudSync]);
1471
+ const refreshWorkspaceSyncContextAssetSnapshots = useCallback(() => {
1472
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1473
+ void refreshWorkspaceSyncAssetsSnapshot();
1474
+ }, [
1475
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1476
+ refreshWorkspaceSyncAssetsSnapshot
1477
+ ]);
2029
1478
  useEffect(() => {
2030
1479
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
2031
1480
  return;
@@ -2041,11 +1490,7 @@ export function useSyncOrchestrator(input) {
2041
1490
  const eventWorkspaceId = String(detail?.workspaceId || '').trim() || 'default';
2042
1491
  if (eventWorkspaceId !== currentWorkspaceId)
2043
1492
  return;
2044
- void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2045
- void refreshWorkspaceSyncAiProfilesSnapshot();
2046
- void refreshWorkspaceSyncAssetsSnapshot();
2047
- void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2048
- void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
1493
+ refreshWorkspaceSyncContextAssetSnapshots();
2049
1494
  };
2050
1495
  window.addEventListener(TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT, handleContextAssetsMutated);
2051
1496
  const docsIntervalId = window.setInterval(() => {
@@ -2078,6 +1523,7 @@ export function useSyncOrchestrator(input) {
2078
1523
  authSessionResolved,
2079
1524
  isAuthenticated,
2080
1525
  currentWorkspaceId,
1526
+ refreshWorkspaceSyncContextAssetSnapshots,
2081
1527
  refreshWorkspaceSyncMarkdownDocumentsSnapshot,
2082
1528
  refreshWorkspaceSyncAiProfilesSnapshot,
2083
1529
  refreshWorkspaceSyncAssetsSnapshot,
@@ -2096,12 +1542,8 @@ export function useSyncOrchestrator(input) {
2096
1542
  return;
2097
1543
  if (!taskAttachmentsFingerprint)
2098
1544
  return;
2099
- void refreshWorkspaceSyncAssetsSnapshot();
2100
- void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2101
- void refreshWorkspaceSyncAiProfilesSnapshot();
2102
- void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2103
- void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
2104
- }, [cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, authSessionResolved, isAuthenticated, taskAttachmentsFingerprint, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot]);
1545
+ refreshWorkspaceSyncContextAssetSnapshots();
1546
+ }, [cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, authSessionResolved, isAuthenticated, taskAttachmentsFingerprint, refreshWorkspaceSyncContextAssetSnapshots]);
2105
1547
  useEffect(() => {
2106
1548
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
2107
1549
  return;
@@ -2134,6 +1576,7 @@ export function useSyncOrchestrator(input) {
2134
1576
  buildWorkspaceSyncSignature,
2135
1577
  shouldSuppressAttachBootstrapPush,
2136
1578
  buildWorkspaceSyncPayload,
1579
+ workspaceCoreSyncFingerprint,
2137
1580
  workspaceSyncAiProfilesFingerprint,
2138
1581
  workspaceSyncMarkdownDocumentsFingerprint,
2139
1582
  workspaceSyncAssetsFingerprint,
@@ -2143,6 +1586,41 @@ export function useSyncOrchestrator(input) {
2143
1586
  workspacePushInFlightRef,
2144
1587
  workspacePullInFlightRef
2145
1588
  ]);
1589
+ useEffect(() => {
1590
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
1591
+ return;
1592
+ if (workspaceSyncPhase !== 'active')
1593
+ return;
1594
+ if (!workspaceSyncReferenceSnapshotsReady)
1595
+ return;
1596
+ const intervalId = window.setInterval(() => {
1597
+ if (workspacePushInFlightRef.current || workspacePullInFlightRef.current)
1598
+ return;
1599
+ if (isWorkspaceRetryPending())
1600
+ return;
1601
+ const signature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
1602
+ if (!signature || signature === workspaceLastPushedSignatureRef.current)
1603
+ return;
1604
+ const pendingPayload = buildWorkspaceSyncPayload();
1605
+ setWorkspaceSyncPendingChanges(pendingPayload.changes.length);
1606
+ if (pendingPayload.changes.length === 0)
1607
+ return;
1608
+ void pushWorkspaceChangesToCloud(signature);
1609
+ }, workspacePushWatchdogIntervalMs);
1610
+ return () => window.clearInterval(intervalId);
1611
+ }, [
1612
+ cloudAuthConfigured,
1613
+ runtimeMode,
1614
+ workspaceCloudSyncEnabled,
1615
+ authSessionResolved,
1616
+ workspaceSyncPhase,
1617
+ workspaceSyncReferenceSnapshotsReady,
1618
+ buildWorkspaceSyncSignature,
1619
+ buildWorkspaceSyncPayload,
1620
+ pushWorkspaceChangesToCloud,
1621
+ isWorkspaceRetryPending,
1622
+ workspacePushWatchdogIntervalMs
1623
+ ]);
2146
1624
  useEffect(() => {
2147
1625
  if (cloudAuthConfigured && runtimeMode === 'local' && workspaceCloudSyncEnabled)
2148
1626
  return;
@@ -2157,25 +1635,54 @@ export function useSyncOrchestrator(input) {
2157
1635
  useEffect(() => {
2158
1636
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
2159
1637
  return;
2160
- if (workspaceSyncPhase === 'provision-local')
1638
+ if (workspaceSyncPhase === 'provision-local' || workspaceSyncPhase === 'error')
2161
1639
  return;
2162
1640
  if (isWorkspaceRetryPending())
2163
1641
  return;
2164
- void pullWorkspaceChangesFromCloud();
1642
+ const shouldKickOffPull = workspaceSyncPhase === 'attach-cloud' || !workspaceLastPullAt;
1643
+ if (shouldKickOffPull) {
1644
+ void pullWorkspaceChangesFromCloud();
1645
+ }
2165
1646
  const intervalId = window.setInterval(() => {
2166
1647
  void pullWorkspaceChangesFromCloud();
2167
1648
  }, workspacePullIntervalMs);
2168
- return () => window.clearInterval(intervalId);
1649
+ return () => {
1650
+ window.clearInterval(intervalId);
1651
+ };
2169
1652
  }, [
2170
1653
  cloudAuthConfigured,
2171
1654
  runtimeMode,
2172
1655
  workspaceCloudSyncEnabled,
2173
1656
  authSessionResolved,
2174
1657
  workspaceSyncPhase,
1658
+ workspaceLastPullAt,
2175
1659
  isWorkspaceRetryPending,
2176
1660
  pullWorkspaceChangesFromCloud,
2177
1661
  workspacePullIntervalMs
2178
1662
  ]);
1663
+ useEffect(() => {
1664
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved || !isAuthenticated)
1665
+ return;
1666
+ if (workspaceSyncPhase !== 'active')
1667
+ return;
1668
+ const intervalId = window.setInterval(() => {
1669
+ if (workspaceFullReconcileInFlightRef.current)
1670
+ return;
1671
+ workspaceFullReconcileInFlightRef.current = true;
1672
+ void pullWorkspaceChangesFromCloud({ forceCursorNull: true }).finally(() => {
1673
+ workspaceFullReconcileInFlightRef.current = false;
1674
+ });
1675
+ }, WORKSPACE_FULL_RECONCILE_INTERVAL_MS);
1676
+ return () => window.clearInterval(intervalId);
1677
+ }, [
1678
+ cloudAuthConfigured,
1679
+ runtimeMode,
1680
+ workspaceCloudSyncEnabled,
1681
+ authSessionResolved,
1682
+ isAuthenticated,
1683
+ workspaceSyncPhase,
1684
+ pullWorkspaceChangesFromCloud
1685
+ ]);
2179
1686
  useEffect(() => {
2180
1687
  if (runtimeMode !== 'local' || !isAuthenticated)
2181
1688
  return;
@@ -2277,6 +1784,7 @@ export function useSyncOrchestrator(input) {
2277
1784
  workspaceLastSuccessfulSyncAt,
2278
1785
  workspaceCloudSyncEnabled,
2279
1786
  workspaceSyncPhase,
1787
+ workspaceSyncSetupIntent,
2280
1788
  workspaceSyncStatus,
2281
1789
  workspaceSyncSummary,
2282
1790
  workspaceSyncRecommendedAction,
@@ -2291,6 +1799,7 @@ export function useSyncOrchestrator(input) {
2291
1799
  workspacePendingSignatureRef,
2292
1800
  workspaceLastPushedTaskIdsRef,
2293
1801
  workspaceDeletedTaskIdsRef,
1802
+ workspaceDeletedTaskWatermarksRef,
2294
1803
  workspacePullCursorRef,
2295
1804
  clearWorkspaceRetry,
2296
1805
  persistWorkspaceSyncPatch,