@taskforcehq/taskforce 0.3.170 → 0.3.301

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 (698) hide show
  1. package/README.md +71 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2227 -1188
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +243 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +180 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3299 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  492. package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
  493. package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
  494. package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
  495. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
  499. package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
  500. package/dist/ui/assets/index-xBWLPOEb.js +6 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +30 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/deploy-prod.sh +41 -0
  610. package/scripts/deploy-staging.sh +23 -0
  611. package/scripts/migrate-planning-model.ts +233 -0
  612. package/scripts/migrate-task-data-assets.mjs +408 -0
  613. package/scripts/playwright-auth.sh +11 -0
  614. package/scripts/run-dev-proxy.sh +2 -0
  615. package/scripts/update-homebrew-formula.sh +2 -2
  616. package/scripts/with-esbuild.sh +11 -0
  617. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  618. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  619. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  620. package/src/resources/templates/workflows/do.yaml +8 -2
  621. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  622. package/src/resources/templates/workflows/execute.yaml +40 -23
  623. package/src/resources/templates/workflows/plan.yaml +15 -8
  624. package/src/resources/templates/workflows/review.yaml +14 -8
  625. package/dist/TaskforceScreenshot.d.ts +0 -11
  626. package/dist/TaskforceScreenshot.js +0 -113
  627. package/dist/compat/priorityCompat.d.ts +0 -2
  628. package/dist/compat/priorityCompat.js +0 -24
  629. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  630. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  631. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  632. package/dist/components/task/TaskKanban 2.js +0 -1004
  633. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  634. package/dist/components/task/TaskScreenshotSection.js +0 -22
  635. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  636. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  637. package/dist/core/InitiativeTemplates.test.js +0 -50
  638. package/dist/core/ParentLifecycle.test.js +0 -98
  639. package/dist/migrations/clientMigrations.d.ts +0 -9
  640. package/dist/migrations/clientMigrations.js +0 -74
  641. package/dist/migrations/flags.d.ts +0 -6
  642. package/dist/migrations/flags.js +0 -8
  643. package/dist/migrations/taskMigrations.d.ts +0 -71
  644. package/dist/migrations/taskMigrations.js +0 -236
  645. package/dist/migrations/taskMigrations.test.js +0 -34
  646. package/dist/server/routes.billing.js +0 -991
  647. package/dist/server/routes.billing.test.js +0 -387
  648. package/dist/server/routes.sync.integration.test.js +0 -470
  649. package/dist/server/routes.sync.js +0 -1141
  650. package/dist/soak/syncSoakHarness.d.ts +0 -6
  651. package/dist/soak/syncSoakHarness.js +0 -10
  652. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  653. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  654. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  655. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  656. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  657. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  658. package/dist/utils/taskHierarchy.d.ts +0 -4
  659. package/dist/utils/taskHierarchy.js +0 -69
  660. package/dist/utils/taskHierarchy.test.js +0 -38
  661. package/dist/utils/taskParenting.d.ts +0 -3
  662. package/dist/utils/taskParenting.js +0 -24
  663. package/dist/utils/taskParenting.test.js +0 -21
  664. package/dist/utils/taskRelations.d.ts +0 -6
  665. package/dist/utils/taskRelations.js +0 -24
  666. package/dist/utils/taskRelations.test.js +0 -34
  667. package/scripts/check-compat-boundaries.js +0 -48
  668. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  669. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  670. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  671. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  672. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  673. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  677. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  678. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  679. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  680. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  681. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  682. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  683. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  684. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  685. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  686. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  687. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  688. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  689. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  690. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  691. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  692. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  693. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  694. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  695. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  696. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  697. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  698. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -1,17 +1,45 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
- import { calculateWorkspaceRetryDelayMs, ensureCloudWorkspaceReadyForSyncService, runWorkspacePullSyncService, runWorkspacePushSyncService, syncUserGlobalSettingsService } from '../sync/syncService';
1
+ /**
2
+ * useSyncOrchestrator React hook that orchestrates all cloud sync activity for a workspace.
3
+ *
4
+ * Sections:
5
+ * - Imports & module-level constants (line ~1)
6
+ * - Pure utility functions (line ~28)
7
+ * - Public interface & hook entry (line ~126)
8
+ * - State initialization (line ~191)
9
+ * - User global settings sync (line ~229)
10
+ * - Workspace sync helpers & bootstrap (line ~516)
11
+ * - Auth failure handling & sync readiness (line ~731)
12
+ * - Push sync (line ~804)
13
+ * - Pull sync (line ~985)
14
+ * - Realtime sync integration (line ~1181)
15
+ * - Repair & manual retry (line ~1239)
16
+ * - Diagnostics & reactive effects (line ~1390)
17
+ * - Return value (line ~1532)
18
+ *
19
+ * Key exports: useSyncOrchestrator, UseSyncOrchestratorInput, WorkspaceSyncStatus
20
+ */
21
+ // === SECTION: Imports & module-level constants ===
22
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
23
+ import { ensureCloudWorkspaceReadyForSyncService, isTransientWorkspaceSyncStatus, runWorkspacePullSyncService, runWorkspacePushSyncService, syncUserGlobalSettingsService } from '../sync/syncService';
3
24
  import { buildWorkspaceSyncPayloadModel, buildWorkspaceSyncSignatureModel } from '../sync/workspaceSyncModel';
25
+ import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
4
26
  import { useWorkspaceSyncController } from './useWorkspaceSyncController';
27
+ import { normalizeTaskforceTheme } from '../utils/theme';
5
28
  import { useRealtimeSync } from './useRealtimeSync';
29
+ import { TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT } from '../utils/contextAssetEvents';
6
30
  import { setLocale as setAppLocale } from '../localization';
7
- // ---------------------------------------------------------------------------
8
- // Module-level constants & helpers (moved from useTaskforce)
9
- // ---------------------------------------------------------------------------
31
+ import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
10
32
  const USER_GLOBAL_SYNC_META_KEY = 'taskforce.userGlobalSyncMeta.v1';
11
33
  const SYNC_DEBUG_LOG_KEY = 'taskforce.syncDebug.v1';
12
- const WORKSPACE_HYDRATION_PUSH_BLOCK_WARNING = 'Local changes queued — waiting for initial cloud pull before upload.';
13
- const WORKSPACE_HYDRATION_PUSH_BLOCK_ESCAPE_MS = 45_000;
14
- const WORKSPACE_HYDRATION_PUSH_BLOCK_RECOVERY_COOLDOWN_MS = 15_000;
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.';
38
+ // === SECTION: Pure utility functions ===
39
+ function isValidWorkspaceKey(workspaceId) {
40
+ const workspaceKey = String(workspaceId || '').trim();
41
+ return workspaceKey.length > 0 && workspaceKey.toLowerCase() !== 'default';
42
+ }
15
43
  function isSyncDebugLoggingEnabled() {
16
44
  if (typeof window === 'undefined')
17
45
  return false;
@@ -29,6 +57,29 @@ function logSyncDebug(event, details) {
29
57
  const payload = details && typeof details === 'object' ? details : {};
30
58
  console.info('[Taskforce Sync]', event, payload);
31
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
+ }
32
83
  function readUserGlobalSyncMeta() {
33
84
  if (typeof window === 'undefined')
34
85
  return {};
@@ -37,7 +88,9 @@ function readUserGlobalSyncMeta() {
37
88
  if (!raw)
38
89
  return {};
39
90
  const parsed = JSON.parse(raw);
40
- return parsed && typeof parsed === 'object' ? parsed : {};
91
+ return parsed && typeof parsed === 'object'
92
+ ? parsed
93
+ : {};
41
94
  }
42
95
  catch {
43
96
  return {};
@@ -53,30 +106,182 @@ function writeUserGlobalSyncMeta(next) {
53
106
  // best-effort only
54
107
  }
55
108
  }
56
- // ---------------------------------------------------------------------------
57
- // Hook
58
- // ---------------------------------------------------------------------------
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
+ function summarizeSyncPhase(enabled, phase, busy, retryAt, lastErrorMessage) {
118
+ if (!enabled)
119
+ return 'Sync is turned off for this workspace.';
120
+ if (phase === 'provision-local') {
121
+ return busy
122
+ ? 'Uploading this workspace to cloud for the first time.'
123
+ : 'Preparing the first upload to cloud.';
124
+ }
125
+ if (phase === 'attach-cloud') {
126
+ return busy
127
+ ? 'Pulling the cloud workspace into this device.'
128
+ : 'Waiting for the first cloud pull into this device.';
129
+ }
130
+ if (retryAt) {
131
+ return 'Sync hit a temporary problem and will retry automatically.';
132
+ }
133
+ if (lastErrorMessage) {
134
+ return 'Sync needs attention before it can continue.';
135
+ }
136
+ if (busy) {
137
+ return 'Sync is currently running.';
138
+ }
139
+ return 'Sync is healthy.';
140
+ }
141
+ function recommendSyncAction(enabled, phase, retryAt, lastErrorMessage) {
142
+ if (!enabled) {
143
+ return 'Turn on sync when you are ready to back up this workspace.';
144
+ }
145
+ if (phase === 'provision-local') {
146
+ return 'Keep this window open while the first upload finishes. If it seems stalled, press Sync.';
147
+ }
148
+ if (phase === 'attach-cloud') {
149
+ return 'Keep this window open for the first cloud pull. If it does not move, press Repair.';
150
+ }
151
+ if (retryAt) {
152
+ return 'Wait for the automatic retry, or press Sync to retry now.';
153
+ }
154
+ if (lastErrorMessage) {
155
+ return 'Press Sync to retry. If the same error keeps returning, press Repair.';
156
+ }
157
+ return 'No action needed.';
158
+ }
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
+ const EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS = {
238
+ documents: false,
239
+ aiProfiles: false,
240
+ assets: false,
241
+ documentReviewSessions: false,
242
+ annotatedAttachmentSessions: false
243
+ };
59
244
  export function useSyncOrchestrator(input) {
60
- const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, taxonomies, setupState, globalTheme, locale, globalWeekStartsOn, themeUseGlobalDefault, setTasks, setArchivedTasks, setAuthBlocked, setIsAuthenticated, checkAuthSession, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn } = input;
61
- // -----------------------------------------------------------------------
62
- // Sync state
63
- // -----------------------------------------------------------------------
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;
246
+ const taxonomyStateFingerprint = useMemo(() => buildTaxonomyStateFingerprint(taxonomyState), [taxonomyState]);
247
+ // === SECTION: State initialization ===
64
248
  const [userGlobalSyncStatus, setUserGlobalSyncStatus] = useState('disconnected');
65
- const [userGlobalLastSyncedAt, setUserGlobalLastSyncedAt] = useState(null);
66
- const [userGlobalSyncPendingChanges, setUserGlobalSyncPendingChanges] = useState(0);
67
249
  const [userGlobalSyncError, setUserGlobalSyncError] = useState(null);
68
250
  const [workspaceLastPullAt, setWorkspaceLastPullAt] = useState(null);
69
251
  const [workspaceLastPushAt, setWorkspaceLastPushAt] = useState(null);
70
252
  const [workspaceLastErrorAt, setWorkspaceLastErrorAt] = useState(null);
71
253
  const [workspaceLastErrorMessage, setWorkspaceLastErrorMessage] = useState(null);
72
- const [workspaceLastWarningMessage, setWorkspaceLastWarningMessage] = useState(null);
73
254
  const [workspaceCloudSyncEnabled, setWorkspaceCloudSyncEnabled] = useState(false);
74
- const [workspaceSyncSourceOfTruth, setWorkspaceSyncSourceOfTruth] = useState(null);
75
- const [workspaceSyncOnboardingCompleted, setWorkspaceSyncOnboardingCompleted] = useState(false);
76
- const [workspaceSyncReady, setWorkspaceSyncReady] = useState(false);
77
- const [workspaceCloudHydrated, setWorkspaceCloudHydrated] = useState(false);
78
- const [workspaceSyncDocuments, setWorkspaceSyncDocuments] = useState([]);
79
- const [workspaceSyncDocumentsFingerprint, setWorkspaceSyncDocumentsFingerprint] = useState('');
255
+ const [workspaceSyncPhase, setWorkspaceSyncPhase] = useState('idle');
256
+ const [workspaceSyncBusy, setWorkspaceSyncBusy] = useState(false);
257
+ const [workspaceSyncPendingChanges, setWorkspaceSyncPendingChanges] = useState(0);
258
+ const [workspaceSyncMarkdownDocuments, setWorkspaceSyncMarkdownDocuments] = useState([]);
259
+ const [workspaceSyncMarkdownDocumentsFingerprint, setWorkspaceSyncMarkdownDocumentsFingerprint] = useState('');
260
+ // Ref mirror kept in sync with state — readable synchronously inside callbacks to avoid
261
+ // stale-closure issues when buildWorkspaceSyncPayload is called after an async refresh.
262
+ const workspaceTasksRef = useRef(tasks || []);
263
+ const workspaceArchivedTasksRef = useRef(archivedTasks || []);
264
+ const workspaceInitiativesRef = useRef(initiatives || []);
265
+ const workspaceWorkstreamsRef = useRef(workstreams || []);
266
+ const workspaceSyncMarkdownDocumentsRef = useRef([]);
267
+ const [workspaceSyncAiProfiles, setWorkspaceSyncAiProfiles] = useState([]);
268
+ const [workspaceSyncAiProfilesFingerprint, setWorkspaceSyncAiProfilesFingerprint] = useState('');
269
+ const [workspaceSyncAiProfilesLastFetchAt, setWorkspaceSyncAiProfilesLastFetchAt] = useState(null);
270
+ const [workspaceSyncAiProfilesLastFetchError, setWorkspaceSyncAiProfilesLastFetchError] = useState(null);
271
+ const [workspaceSyncAiProfilesRawCount, setWorkspaceSyncAiProfilesRawCount] = useState(0);
272
+ const [workspaceSyncAiProfilesLastSkipReason, setWorkspaceSyncAiProfilesLastSkipReason] = useState(null);
273
+ const workspaceSyncAiProfilesRef = useRef([]);
274
+ const [workspaceSyncAssets, setWorkspaceSyncAssets] = useState([]);
275
+ const [workspaceSyncAssetsFingerprint, setWorkspaceSyncAssetsFingerprint] = useState('');
276
+ const workspaceSyncAssetsRef = useRef([]);
277
+ const [workspaceSyncDocumentReviewSessions, setWorkspaceSyncDocumentReviewSessions] = useState([]);
278
+ const [workspaceSyncDocumentReviewSessionsFingerprint, setWorkspaceSyncDocumentReviewSessionsFingerprint] = useState('');
279
+ const workspaceSyncDocumentReviewSessionsRef = useRef([]);
280
+ const workspaceSyncDocumentReviewCommentsRef = useRef([]);
281
+ const [workspaceSyncAnnotatedAttachmentSessions, setWorkspaceSyncAnnotatedAttachmentSessions] = useState([]);
282
+ const [workspaceSyncAnnotatedAttachmentSessionsFingerprint, setWorkspaceSyncAnnotatedAttachmentSessionsFingerprint] = useState('');
283
+ const workspaceSyncAnnotatedAttachmentSessionsRef = useRef([]);
284
+ const [workspaceSyncReferenceSnapshotStatus, setWorkspaceSyncReferenceSnapshotStatus] = useState(EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS);
80
285
  const [realtimeConnectionState, setRealtimeConnectionState] = useState('degraded-fallback');
81
286
  const [realtimeTelemetry, setRealtimeTelemetry] = useState({
82
287
  accepted: 0,
@@ -85,38 +290,27 @@ export function useSyncOrchestrator(input) {
85
290
  invalidDiscarded: 0,
86
291
  recoveryTriggered: 0
87
292
  });
88
- // -----------------------------------------------------------------------
89
- // Sync refs
90
- // -----------------------------------------------------------------------
91
293
  const userSettingsSyncInFlightRef = useRef(false);
92
294
  const userSettingsRetryTimeoutRef = useRef(null);
93
295
  const userSettingsRetryAttemptRef = useRef(0);
94
296
  const applyingRemoteUserSettingsRef = useRef(false);
95
297
  const initialUserSettingsSyncCompletedRef = useRef(new Set());
96
298
  const lastUserGlobalSyncSignatureRef = useRef('');
97
- const workspaceSyncDocumentsFingerprintRef = useRef('');
299
+ const workspaceSyncMarkdownDocumentsFingerprintRef = useRef('');
300
+ const workspaceSyncAiProfilesFingerprintRef = useRef('');
301
+ const workspaceSyncAssetsFingerprintRef = useRef('');
302
+ const workspaceSyncDocumentReviewSessionsFingerprintRef = useRef('');
303
+ const workspaceSyncAnnotatedAttachmentSessionsFingerprintRef = useRef('');
98
304
  const syncInFlightRef = useRef(false);
99
- const workspacePushBlockedSinceRef = useRef(new Map());
100
- const workspacePushBlockedRecoveryAtRef = useRef(new Map());
101
305
  const realtimePullDebounceRef = useRef(null);
102
306
  const realtimeSuppressedEventIdsRef = useRef(new Set());
103
307
  const realtimeSuppressedEventQueueRef = useRef([]);
104
- const realtimeTelemetryReportedAtRef = useRef(0);
105
- const realtimeTelemetrySnapshotRef = useRef({
106
- accepted: 0,
107
- duplicateDiscarded: 0,
108
- outOfOrderDiscarded: 0,
109
- invalidDiscarded: 0,
110
- recoveryTriggered: 0
111
- });
112
- // Stable ref so ensureWorkspaceSyncReady can schedule a retry via pullFromCloud
113
- // without creating a forward-reference dependency cycle.
114
- const pullFromCloudRef = useRef(() => Promise.resolve(false));
115
- // Mirror of workspaceSyncOnboardingCompleted state — kept as a ref so the
116
- // pullWorkspaceChangesFromCloud callback can read the current value without
117
- // needing to be re-created every time the state changes (stale closure guard).
118
- const onboardingCompletedRef = useRef(false);
308
+ const retryWorkspaceSyncRef = useRef(() => Promise.resolve(false));
119
309
  const syncUserGlobalSettingsRef = useRef(async () => { });
310
+ const bootstrapPhaseStartedAtRef = useRef(null);
311
+ const bootstrapLastProgressAtRef = useRef(null);
312
+ const lastBootstrapPhaseRef = useRef('idle');
313
+ // === SECTION: User global settings sync ===
120
314
  const clearUserSettingsRetry = useCallback(() => {
121
315
  if (userSettingsRetryTimeoutRef.current !== null) {
122
316
  window.clearTimeout(userSettingsRetryTimeoutRef.current);
@@ -129,59 +323,160 @@ export function useSyncOrchestrator(input) {
129
323
  clearUserSettingsRetry();
130
324
  const nextAttempt = Math.max(1, Math.floor(userSettingsRetryAttemptRef.current) + 1);
131
325
  userSettingsRetryAttemptRef.current = nextAttempt;
132
- const computedDelay = calculateWorkspaceRetryDelayMs(nextAttempt);
133
- const retryAfterFloor = Number.isFinite(Number(minDelayMs)) ? Math.max(0, Math.floor(Number(minDelayMs))) : 0;
134
- const delayMs = Math.max(computedDelay, retryAfterFloor);
326
+ const computedDelay = 1500 * (2 ** Math.max(0, nextAttempt - 1));
327
+ const retryAfterFloor = Number.isFinite(Number(minDelayMs))
328
+ ? Math.max(0, Math.floor(Number(minDelayMs)))
329
+ : 0;
330
+ const delayMs = Math.max(Math.min(120_000, computedDelay), retryAfterFloor);
135
331
  userSettingsRetryTimeoutRef.current = window.setTimeout(() => {
136
332
  userSettingsRetryTimeoutRef.current = null;
137
333
  void syncUserGlobalSettingsRef.current({ preferCloudOnFirstSync: false });
138
334
  }, delayMs);
139
335
  }, [clearUserSettingsRetry]);
140
- // -----------------------------------------------------------------------
141
- // Reset document fingerprint and push watermarks when workspace changes
142
- // -----------------------------------------------------------------------
143
336
  useEffect(() => {
144
- workspaceSyncDocumentsFingerprintRef.current = '';
145
- setWorkspaceSyncDocuments([]);
146
- setWorkspaceSyncDocumentsFingerprint('');
147
- setWorkspaceLastWarningMessage(null);
148
- workspacePushBlockedSinceRef.current.delete(currentWorkspaceId);
149
- workspacePushBlockedRecoveryAtRef.current.delete(currentWorkspaceId);
337
+ workspaceSyncMarkdownDocumentsFingerprintRef.current = '';
338
+ workspaceSyncAiProfilesFingerprintRef.current = '';
339
+ workspaceSyncAssetsFingerprintRef.current = '';
340
+ workspaceSyncDocumentReviewSessionsFingerprintRef.current = '';
341
+ workspaceSyncAnnotatedAttachmentSessionsFingerprintRef.current = '';
342
+ workspaceAttachBootstrapBaselineSignatureRef.current = '';
343
+ workspaceCaptureAttachBootstrapBaselineRef.current = false;
344
+ setWorkspaceSyncMarkdownDocuments([]);
345
+ setWorkspaceSyncMarkdownDocumentsFingerprint('');
346
+ workspaceSyncAiProfilesRef.current = [];
347
+ setWorkspaceSyncAiProfiles([]);
348
+ setWorkspaceSyncAiProfilesFingerprint('');
349
+ setWorkspaceSyncAssets([]);
350
+ setWorkspaceSyncAssetsFingerprint('');
351
+ workspaceSyncDocumentReviewSessionsRef.current = [];
352
+ workspaceSyncDocumentReviewCommentsRef.current = [];
353
+ setWorkspaceSyncDocumentReviewSessions([]);
354
+ setWorkspaceSyncDocumentReviewSessionsFingerprint('');
355
+ workspaceSyncAnnotatedAttachmentSessionsRef.current = [];
356
+ setWorkspaceSyncAnnotatedAttachmentSessions([]);
357
+ setWorkspaceSyncAnnotatedAttachmentSessionsFingerprint('');
358
+ setWorkspaceSyncReferenceSnapshotStatus(EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS);
359
+ setWorkspaceLastErrorMessage(null);
360
+ setWorkspaceLastErrorAt(null);
361
+ setWorkspaceSyncPendingChanges(0);
362
+ realtimeSuppressedEventIdsRef.current = new Set();
363
+ realtimeSuppressedEventQueueRef.current = [];
364
+ // Reset watermarks for the new workspace, then restore from localStorage if recent
365
+ // enough. This prevents a full resync on every page reload while still catching any
366
+ // changes that happened while the app was closed.
150
367
  workspaceLastPushedWatermarksRef.current = new Map();
368
+ workspaceLastPushedInitiativeIdsRef.current = new Set();
369
+ workspaceLastPushedInitiativeWatermarksRef.current = new Map();
370
+ workspaceLastPushedWorkstreamIdsRef.current = new Set();
371
+ workspaceLastPushedWorkstreamWatermarksRef.current = new Map();
372
+ workspaceLastPushedAiProfileIdsRef.current = new Set();
373
+ workspaceLastPushedAiProfileWatermarksRef.current = new Map();
151
374
  workspaceLastPushedDocumentPathsRef.current = new Set();
152
375
  workspaceLastPushedDocumentWatermarksRef.current = new Map();
153
- realtimeSuppressedEventIdsRef.current = new Set();
154
- realtimeSuppressedEventQueueRef.current = [];
155
- }, [currentWorkspaceId]);
156
- // -----------------------------------------------------------------------
157
- // useWorkspaceSyncController
158
- // -----------------------------------------------------------------------
159
- const handleFailureRecorded = useCallback(() => {
160
- setWorkspaceLastErrorAt(new Date().toISOString());
376
+ workspaceLastPushedAssetPathsRef.current = new Set();
377
+ workspaceLastPushedAssetWatermarksRef.current = new Map();
378
+ workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set();
379
+ workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map();
380
+ workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set();
381
+ workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map();
382
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
383
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
384
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
385
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
386
+ const persisted = readPersistedSyncWatermarks(currentWorkspaceId, taxonomyStateFingerprint);
387
+ if (persisted) {
388
+ workspaceLastPushedWatermarksRef.current = persisted.taskWatermarks;
389
+ workspaceLastPushedInitiativeWatermarksRef.current = persisted.initiativeWatermarks;
390
+ workspaceLastPushedWorkstreamWatermarksRef.current = persisted.workstreamWatermarks;
391
+ workspaceLastPushedDocumentWatermarksRef.current = persisted.documentWatermarks;
392
+ workspaceLastPushedAssetWatermarksRef.current = persisted.assetWatermarks;
393
+ workspaceLastPushedDocumentReviewSessionWatermarksRef.current = persisted.documentReviewSessionWatermarks;
394
+ workspaceLastPushedDocumentReviewCommentWatermarksRef.current = persisted.documentReviewCommentWatermarks;
395
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = persisted.annotatedAttachmentSessionWatermarks;
396
+ }
397
+ }, [currentWorkspaceId, taxonomyStateFingerprint]);
398
+ const markWorkspaceSyncReferenceSnapshotReady = useCallback((key) => {
399
+ setWorkspaceSyncReferenceSnapshotStatus((current) => (current[key]
400
+ ? current
401
+ : {
402
+ ...current,
403
+ [key]: true
404
+ }));
161
405
  }, []);
162
- const { workspaceRetryAt, isWorkspacePullRetryPending, workspaceBootstrapSyncInProgress, setWorkspaceBootstrapSyncInProgress, workspaceBootstrapSyncPages, setWorkspaceBootstrapSyncPages, workspaceBootstrapSyncAppliedChanges, setWorkspaceBootstrapSyncAppliedChanges, workspaceSyncFailureCount, workspaceSyncRetryScheduledCount, workspaceSyncAuthFailureCount, workspaceSyncLastFailureSource, workspaceSyncLastFailureStatusCode, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, workspacePullBootstrapDoneRef, workspaceBackfillForcedRef, workspaceSyncReadyRef, workspaceCloudHydratedRef, clearWorkspaceRetry, scheduleWorkspacePullRetry, isWorkspacePullCooldownActive, markWorkspacePullCooldown, tryAcquireWorkspacePullLease, releaseWorkspacePullLease, recordWorkspaceSyncFailure, incrementWorkspaceSyncAuthFailure, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase } = useWorkspaceSyncController({
406
+ 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({
163
408
  currentWorkspaceId,
164
- setWorkspaceSyncReady,
165
- setWorkspaceCloudHydrated,
166
- onFailureRecorded: handleFailureRecorded,
167
409
  setWorkspaceCloudSyncEnabled,
168
- setWorkspaceSyncSourceOfTruth,
169
- setWorkspaceSyncOnboardingCompleted,
170
- setUserGlobalLastSyncedAt,
410
+ setWorkspaceSyncPhase,
171
411
  setWorkspaceLastPullAt,
172
- setWorkspaceLastPushAt
412
+ setWorkspaceLastPushAt,
413
+ setWorkspaceLastErrorMessage
173
414
  });
174
- // -----------------------------------------------------------------------
175
- // Per-task watermark map: tracks the updatedAt|createdAt of each task as of its last
176
- // successful push. Used by buildWorkspaceSyncPayload to skip unchanged tasks (dirty push).
177
415
  const workspaceLastPushedWatermarksRef = useRef(new Map());
178
- // Tracks document paths present at last successful push so removed docs can emit
179
- // document-delete operations on the next payload build.
416
+ const workspaceLastPushedInitiativeIdsRef = useRef(new Set());
417
+ const workspaceLastPushedInitiativeWatermarksRef = useRef(new Map());
418
+ const workspaceLastPushedWorkstreamIdsRef = useRef(new Set());
419
+ const workspaceLastPushedWorkstreamWatermarksRef = useRef(new Map());
420
+ const workspaceLastPushedAiProfileIdsRef = useRef(new Set());
421
+ const workspaceLastPushedAiProfileWatermarksRef = useRef(new Map());
180
422
  const workspaceLastPushedDocumentPathsRef = useRef(new Set());
181
- // Tracks last pushed document updatedAt per path to reduce unchanged document-upsert churn.
182
423
  const workspaceLastPushedDocumentWatermarksRef = useRef(new Map());
183
- // User global settings sync helpers
184
- // -----------------------------------------------------------------------
424
+ const workspaceLastPushedAssetPathsRef = useRef(new Set());
425
+ const workspaceLastPushedAssetWatermarksRef = useRef(new Map());
426
+ const workspaceLastPushedDocumentReviewSessionIdsRef = useRef(new Set());
427
+ const workspaceLastPushedDocumentReviewSessionWatermarksRef = useRef(new Map());
428
+ const workspaceLastPushedDocumentReviewCommentIdsRef = useRef(new Set());
429
+ const workspaceLastPushedDocumentReviewCommentWatermarksRef = useRef(new Map());
430
+ const workspaceLastPushedAnnotatedAttachmentSessionIdsRef = useRef(new Set());
431
+ const workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef = useRef(new Map());
432
+ const workspaceStartupRepairRanRef = useRef(false);
433
+ const workspaceRepairModeRef = useRef(false);
434
+ const workspaceAttachBootstrapBaselineSignatureRef = useRef('');
435
+ const workspaceCaptureAttachBootstrapBaselineRef = useRef(false);
436
+ const workspaceDocumentsRefreshIntervalMs = 30_000;
437
+ const workspacePullIntervalMs = realtimeSyncEnabled ? 30_000 : 15_000;
438
+ const workspaceLastSuccessfulSyncAt = useMemo(() => {
439
+ const pullMs = workspaceLastPullAt ? Date.parse(workspaceLastPullAt) : NaN;
440
+ const pushMs = workspaceLastPushAt ? Date.parse(workspaceLastPushAt) : NaN;
441
+ if (Number.isFinite(pullMs) && Number.isFinite(pushMs)) {
442
+ return pullMs >= pushMs ? workspaceLastPullAt : workspaceLastPushAt;
443
+ }
444
+ if (Number.isFinite(pullMs))
445
+ return workspaceLastPullAt;
446
+ if (Number.isFinite(pushMs))
447
+ return workspaceLastPushAt;
448
+ return null;
449
+ }, [workspaceLastPullAt, workspaceLastPushAt]);
450
+ const workspaceSyncStatus = useMemo(() => {
451
+ if (!workspaceCloudSyncEnabled)
452
+ return 'off';
453
+ if (workspaceSyncBusy || workspaceSyncPhase === 'provision-local' || workspaceSyncPhase === 'attach-cloud') {
454
+ return 'syncing';
455
+ }
456
+ if (workspaceRetryAt || workspaceLastErrorMessage || workspaceSyncPhase === 'error') {
457
+ return 'attention';
458
+ }
459
+ return 'healthy';
460
+ }, [
461
+ workspaceCloudSyncEnabled,
462
+ workspaceSyncBusy,
463
+ workspaceSyncPhase,
464
+ workspaceRetryAt,
465
+ workspaceLastErrorMessage
466
+ ]);
467
+ const workspaceSyncSummary = useMemo(() => summarizeSyncPhase(workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncBusy, workspaceRetryAt, workspaceLastErrorMessage), [
468
+ workspaceCloudSyncEnabled,
469
+ workspaceSyncPhase,
470
+ workspaceSyncBusy,
471
+ workspaceRetryAt,
472
+ workspaceLastErrorMessage
473
+ ]);
474
+ const workspaceSyncRecommendedAction = useMemo(() => recommendSyncAction(workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceRetryAt, workspaceLastErrorMessage), [
475
+ workspaceCloudSyncEnabled,
476
+ workspaceSyncPhase,
477
+ workspaceRetryAt,
478
+ workspaceLastErrorMessage
479
+ ]);
185
480
  const getLocalUserSyncUpdatedAt = useCallback((userId) => {
186
481
  const key = String(userId || '').trim();
187
482
  if (!key)
@@ -216,14 +511,15 @@ export function useSyncOrchestrator(input) {
216
511
  applyingRemoteUserSettingsRef.current = true;
217
512
  try {
218
513
  const patch = {};
219
- if (remote.theme === 'dark' || remote.theme === 'light' || remote.theme === 'midnight') {
220
- setGlobalTheme(remote.theme);
514
+ const remoteTheme = normalizeTaskforceTheme(remote.theme);
515
+ if (remoteTheme) {
516
+ setGlobalTheme(remoteTheme);
221
517
  if (themeUseGlobalDefault)
222
- setCurrentTheme(remote.theme);
223
- patch.theme = remote.theme;
518
+ setCurrentTheme(remoteTheme);
519
+ patch.theme = remoteTheme;
224
520
  }
225
521
  if (remote.operatingMode === 'core' || remote.operatingMode === 'operations') {
226
- setSetupState((prev) => prev ? { ...prev, mode: remote.operatingMode } : prev);
522
+ setSetupState((prev) => (prev ? { ...prev, mode: remote.operatingMode } : prev));
227
523
  patch.setup = { mode: remote.operatingMode };
228
524
  }
229
525
  if (remote.localization && typeof remote.localization === 'object') {
@@ -253,7 +549,14 @@ export function useSyncOrchestrator(input) {
253
549
  finally {
254
550
  applyingRemoteUserSettingsRef.current = false;
255
551
  }
256
- }, [themeUseGlobalDefault, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn]);
552
+ }, [
553
+ themeUseGlobalDefault,
554
+ setGlobalTheme,
555
+ setCurrentTheme,
556
+ setSetupState,
557
+ setLocale,
558
+ setGlobalWeekStartsOn
559
+ ]);
257
560
  const syncUserGlobalSettings = useCallback(async (options) => {
258
561
  const preferCloudOnFirstSync = options?.preferCloudOnFirstSync !== false;
259
562
  if (!cloudAuthConfigured)
@@ -302,8 +605,6 @@ export function useSyncOrchestrator(input) {
302
605
  userSettingsRetryAttemptRef.current = 0;
303
606
  const syncedAt = String(syncResult.updatedAt || '').trim() || new Date().toISOString();
304
607
  setLocalUserSyncUpdatedAt(userId, syncedAt);
305
- setUserGlobalLastSyncedAt(syncedAt);
306
- setUserGlobalSyncPendingChanges(0);
307
608
  setUserGlobalSyncStatus('idle');
308
609
  setUserGlobalSyncError(null);
309
610
  initialUserSettingsSyncCompletedRef.current.add(userId);
@@ -320,15 +621,15 @@ export function useSyncOrchestrator(input) {
320
621
  userSettingsSyncInFlightRef.current = false;
321
622
  }
322
623
  }, [
624
+ cloudAuthConfigured,
323
625
  runtimeMode,
324
626
  isAuthenticated,
325
627
  authUserId,
628
+ resolveCloudAuthUrl,
326
629
  getLocalUserSyncUpdatedAt,
327
- setLocalUserSyncUpdatedAt,
328
- applyRemoteUserGlobalSyncPayload,
329
630
  buildUserGlobalSyncPayload,
330
- resolveCloudAuthUrl,
331
- cloudAuthConfigured,
631
+ applyRemoteUserGlobalSyncPayload,
632
+ setLocalUserSyncUpdatedAt,
332
633
  clearUserSettingsRetry,
333
634
  scheduleUserSettingsRetry
334
635
  ]);
@@ -340,9 +641,74 @@ export function useSyncOrchestrator(input) {
340
641
  clearUserSettingsRetry();
341
642
  };
342
643
  }, [clearUserSettingsRetry]);
343
- // -----------------------------------------------------------------------
344
- // Workspace sync helpers
345
- // -----------------------------------------------------------------------
644
+ // === SECTION: Workspace sync helpers & bootstrap ===
645
+ const clearWorkspaceIssue = useCallback(() => {
646
+ setWorkspaceLastErrorMessage(null);
647
+ setWorkspaceLastErrorAt(null);
648
+ }, []);
649
+ const reportWorkspaceWindowContention = useCallback((operation) => {
650
+ setWorkspaceLastErrorMessage(WORKSPACE_CONCURRENT_WINDOW_MESSAGE);
651
+ setWorkspaceLastErrorAt(new Date().toISOString());
652
+ setUserGlobalSyncStatus('error');
653
+ setUserGlobalSyncError(WORKSPACE_CONCURRENT_WINDOW_MESSAGE);
654
+ logSyncDebug('workspace_sync_window_contention', {
655
+ workspaceId: currentWorkspaceId,
656
+ operation
657
+ });
658
+ }, [currentWorkspaceId]);
659
+ const persistWorkspaceSyncPatchSafely = useCallback((patch) => {
660
+ void persistWorkspaceSyncPatch(patch).catch(() => { });
661
+ }, [persistWorkspaceSyncPatch]);
662
+ const scheduleWorkspaceSyncRetry = useCallback((minDelayMs) => {
663
+ scheduleWorkspaceRetry(() => retryWorkspaceSyncRef.current(), { minDelayMs });
664
+ }, [scheduleWorkspaceRetry]);
665
+ 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;
676
+ }, [workspaceSyncPhase]);
677
+ 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);
704
+ }, [
705
+ currentWorkspaceId,
706
+ workspaceCloudSyncEnabled,
707
+ workspaceSyncPhase,
708
+ persistWorkspaceSyncPatchSafely,
709
+ workspacePullInFlightRef,
710
+ workspacePushInFlightRef
711
+ ]);
346
712
  const ensureCloudWorkspaceReadyForSync = useCallback(async () => {
347
713
  return ensureCloudWorkspaceReadyForSyncService({
348
714
  cloudAuthConfigured,
@@ -360,20 +726,58 @@ export function useSyncOrchestrator(input) {
360
726
  currentWorkspaceId,
361
727
  projectName
362
728
  ]);
363
- const buildWorkspaceSyncPayload = useCallback(() => {
729
+ useEffect(() => {
730
+ workspaceTasksRef.current = tasks || [];
731
+ }, [tasks]);
732
+ useEffect(() => {
733
+ workspaceArchivedTasksRef.current = archivedTasks || [];
734
+ }, [archivedTasks]);
735
+ useEffect(() => {
736
+ workspaceInitiativesRef.current = initiatives || [];
737
+ }, [initiatives]);
738
+ useEffect(() => {
739
+ workspaceWorkstreamsRef.current = workstreams || [];
740
+ }, [workstreams]);
741
+ const workspaceForceFullAiProfilePushRef = useRef(false);
742
+ const buildWorkspaceSyncPayload = useCallback((options) => {
743
+ const forceAllAiProfiles = options?.forceAllAiProfiles === true;
744
+ // Read documents and assets from refs rather than state so that the freshest data
745
+ // fetched by an immediately-preceding async refresh is used, even when React has not
746
+ // yet committed the corresponding state update (stale-closure guard).
364
747
  return buildWorkspaceSyncPayloadModel({
365
- tasks: tasks || [],
366
- archivedTasks: archivedTasks || [],
748
+ tasks: workspaceTasksRef.current,
749
+ archivedTasks: workspaceArchivedTasksRef.current,
367
750
  lastPushedTaskIds: workspaceLastPushedTaskIdsRef.current,
368
751
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
752
+ initiatives: workspaceInitiativesRef.current,
753
+ workstreams: workspaceWorkstreamsRef.current,
369
754
  taxonomies: taxonomies || [],
370
- documents: workspaceSyncDocuments || [],
755
+ taxonomyState,
756
+ lastPushedInitiativeIds: workspaceLastPushedInitiativeIdsRef.current,
757
+ lastPushedInitiativeWatermarks: workspaceLastPushedInitiativeWatermarksRef.current,
758
+ lastPushedWorkstreamIds: workspaceLastPushedWorkstreamIdsRef.current,
759
+ lastPushedWorkstreamWatermarks: workspaceLastPushedWorkstreamWatermarksRef.current,
760
+ aiProfiles: workspaceSyncAiProfilesRef.current,
761
+ lastPushedAiProfileIds: forceAllAiProfiles ? new Set() : workspaceLastPushedAiProfileIdsRef.current,
762
+ lastPushedAiProfileWatermarks: forceAllAiProfiles ? new Map() : workspaceLastPushedAiProfileWatermarksRef.current,
763
+ documents: workspaceSyncMarkdownDocumentsRef.current,
764
+ assets: workspaceSyncAssetsRef.current,
765
+ documentReviewSessions: workspaceSyncDocumentReviewSessionsRef.current,
766
+ documentReviewComments: workspaceSyncDocumentReviewCommentsRef.current,
767
+ annotatedAttachmentSessions: workspaceSyncAnnotatedAttachmentSessionsRef.current,
371
768
  lastPushedDocumentPaths: workspaceLastPushedDocumentPathsRef.current,
372
769
  lastPushedDocumentWatermarks: workspaceLastPushedDocumentWatermarksRef.current,
770
+ lastPushedAssetPaths: workspaceLastPushedAssetPathsRef.current,
771
+ lastPushedAssetWatermarks: workspaceLastPushedAssetWatermarksRef.current,
772
+ lastPushedDocumentReviewSessionIds: workspaceLastPushedDocumentReviewSessionIdsRef.current,
773
+ lastPushedDocumentReviewSessionWatermarks: workspaceLastPushedDocumentReviewSessionWatermarksRef.current,
774
+ lastPushedDocumentReviewCommentIds: workspaceLastPushedDocumentReviewCommentIdsRef.current,
775
+ lastPushedDocumentReviewCommentWatermarks: workspaceLastPushedDocumentReviewCommentWatermarksRef.current,
776
+ lastPushedAnnotatedAttachmentSessionIds: workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current,
777
+ lastPushedAnnotatedAttachmentSessionWatermarks: workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current,
373
778
  lastPushedWatermarks: workspaceLastPushedWatermarksRef.current
374
779
  });
375
- }, [tasks, archivedTasks, taxonomies, workspaceSyncDocuments]);
376
- // Fire-and-forget: persist browser-side sync event to the local SQLite event log.
780
+ }, [taxonomies, taxonomyState]);
377
781
  const reportSyncEvent = useCallback((workspaceId, event) => {
378
782
  if (runtimeMode !== 'local')
379
783
  return;
@@ -392,23 +796,66 @@ export function useSyncOrchestrator(input) {
392
796
  });
393
797
  }, [runtimeMode]);
394
798
  const buildWorkspaceSyncSignature = useCallback(() => {
799
+ // Read documents and assets from refs for the same stale-closure reason as
800
+ // buildWorkspaceSyncPayload — the signature must reflect the freshest known data.
395
801
  return buildWorkspaceSyncSignatureModel({
396
802
  workspaceId: currentWorkspaceId,
397
- tasks: tasks || [],
398
- archivedTasks: archivedTasks || [],
803
+ tasks: workspaceTasksRef.current,
804
+ archivedTasks: workspaceArchivedTasksRef.current,
399
805
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
806
+ initiatives: workspaceInitiativesRef.current,
807
+ workstreams: workspaceWorkstreamsRef.current,
400
808
  taxonomies: taxonomies || [],
401
- documents: workspaceSyncDocuments || []
809
+ aiProfiles: workspaceSyncAiProfilesRef.current,
810
+ documents: workspaceSyncMarkdownDocumentsRef.current,
811
+ assets: workspaceSyncAssetsRef.current,
812
+ documentReviewSessions: workspaceSyncDocumentReviewSessionsRef.current,
813
+ documentReviewComments: workspaceSyncDocumentReviewCommentsRef.current,
814
+ annotatedAttachmentSessions: workspaceSyncAnnotatedAttachmentSessionsRef.current
402
815
  });
403
- }, [tasks, archivedTasks, currentWorkspaceId, taxonomies, workspaceSyncDocuments]);
404
- const refreshWorkspaceSyncDocumentsSnapshot = useCallback(async () => {
816
+ }, [currentWorkspaceId, taxonomies]);
817
+ // Fingerprint of per-task attachment counts — changes when the MCP tool saves a new
818
+ // attachment and the React app polls the updated task. Used to trigger an immediate
819
+ // asset snapshot refresh without requiring a cross-process DOM event.
820
+ const taskAttachmentsFingerprint = useMemo(() => {
821
+ const parts = [];
822
+ for (const task of tasks || []) {
823
+ const count = Array.isArray(task.attachments) ? task.attachments.length : 0;
824
+ if (count > 0)
825
+ parts.push(`${task.id}:${count}`);
826
+ }
827
+ return parts.join('|');
828
+ }, [tasks]);
829
+ const shouldSuppressAttachBootstrapPush = useCallback((signature) => {
830
+ if (!signature)
831
+ return false;
832
+ if (workspaceCaptureAttachBootstrapBaselineRef.current) {
833
+ workspaceCaptureAttachBootstrapBaselineRef.current = false;
834
+ workspaceAttachBootstrapBaselineSignatureRef.current = signature;
835
+ workspaceLastPushedSignatureRef.current = signature;
836
+ setWorkspaceSyncPendingChanges(0);
837
+ return true;
838
+ }
839
+ const baselineSignature = workspaceAttachBootstrapBaselineSignatureRef.current;
840
+ if (!baselineSignature)
841
+ return false;
842
+ if (signature === baselineSignature) {
843
+ workspaceLastPushedSignatureRef.current = signature;
844
+ setWorkspaceSyncPendingChanges(0);
845
+ return true;
846
+ }
847
+ workspaceAttachBootstrapBaselineSignatureRef.current = '';
848
+ return false;
849
+ }, []);
850
+ const refreshWorkspaceSyncMarkdownDocumentsSnapshot = useCallback(async () => {
405
851
  if (runtimeMode !== 'local')
406
852
  return;
407
- const workspaceKey = String(currentWorkspaceId || '').trim();
408
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
853
+ if (!isValidWorkspaceKey(currentWorkspaceId))
854
+ return;
855
+ if (cloudAuthConfigured && (!authSessionResolved || !isAuthenticated))
409
856
  return;
410
857
  try {
411
- const res = await fetch(`/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceKey)}`, {
858
+ const res = await fetch(`/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
412
859
  method: 'GET',
413
860
  credentials: 'include'
414
861
  });
@@ -420,188 +867,522 @@ export function useSyncOrchestrator(input) {
420
867
  .map((entry) => ({
421
868
  path: String(entry?.path || '').trim(),
422
869
  updatedAt: String(entry?.updatedAt || '').trim(),
423
- content: typeof entry?.content === 'string' ? entry.content : ''
870
+ content: typeof entry?.content === 'string' ? entry.content : '',
871
+ assetId: typeof entry?.assetId === 'string' && entry.assetId.trim().length > 0 ? entry.assetId.trim() : undefined,
872
+ documentId: typeof entry?.documentId === 'string' && entry.documentId.trim().length > 0 ? entry.documentId.trim() : null,
873
+ referenceNumber: Number.isFinite(Number(entry?.referenceNumber)) ? Math.max(1, Math.floor(Number(entry.referenceNumber))) : null,
874
+ version: Number.isFinite(Number(entry?.version)) ? Math.max(1, Math.floor(Number(entry.version))) : null,
875
+ taskId: typeof entry?.taskId === 'string' && entry.taskId.trim().length > 0 ? entry.taskId.trim() : null,
876
+ logicalName: typeof entry?.logicalName === 'string' && entry.logicalName.trim().length > 0 ? entry.logicalName.trim() : null,
877
+ caption: typeof entry?.caption === 'string' && entry.caption.trim().length > 0 ? entry.caption.trim() : null,
878
+ originalFilename: typeof entry?.originalFilename === 'string' && entry.originalFilename.trim().length > 0 ? entry.originalFilename.trim() : null,
879
+ linkRole: entry?.linkRole === 'reference' ? 'reference' : 'attachment'
424
880
  }))
425
881
  .filter((entry) => entry.path.length > 0)
426
882
  : [];
427
883
  const fingerprint = typeof data?.fingerprint === 'string'
428
884
  ? data.fingerprint
429
885
  : JSON.stringify(docs.map((entry) => `${entry.path}:${entry.updatedAt}:${entry.content.length}`).sort());
430
- if (fingerprint === workspaceSyncDocumentsFingerprintRef.current)
886
+ if (fingerprint === workspaceSyncMarkdownDocumentsFingerprintRef.current)
431
887
  return;
432
- workspaceSyncDocumentsFingerprintRef.current = fingerprint;
433
- setWorkspaceSyncDocuments(docs);
434
- setWorkspaceSyncDocumentsFingerprint(fingerprint);
888
+ workspaceSyncMarkdownDocumentsFingerprintRef.current = fingerprint;
889
+ workspaceSyncMarkdownDocumentsRef.current = docs;
890
+ setWorkspaceSyncMarkdownDocuments(docs);
891
+ setWorkspaceSyncMarkdownDocumentsFingerprint(fingerprint);
435
892
  }
436
893
  catch (error) {
437
- const detail = String(error?.message || error || '');
438
894
  logSyncDebug('documents_snapshot_refresh_failed', {
439
- workspaceId: workspaceKey,
440
- error: detail
895
+ workspaceId: currentWorkspaceId,
896
+ error: String(error?.message || error || '')
441
897
  });
442
- reportSyncEvent(workspaceKey, {
443
- eventType: 'push',
444
- status: 'error',
445
- errorMessage: `Workspace documents snapshot refresh failed: ${detail}`
898
+ }
899
+ finally {
900
+ markWorkspaceSyncReferenceSnapshotReady('documents');
901
+ }
902
+ }, [runtimeMode, currentWorkspaceId, cloudAuthConfigured, authSessionResolved, isAuthenticated, markWorkspaceSyncReferenceSnapshotReady]);
903
+ const refreshWorkspaceSyncAiProfilesSnapshot = useCallback(async () => {
904
+ if (runtimeMode !== 'local') {
905
+ setWorkspaceSyncAiProfilesLastSkipReason('runtime-not-local');
906
+ return;
907
+ }
908
+ if (!isValidWorkspaceKey(currentWorkspaceId)) {
909
+ setWorkspaceSyncAiProfilesLastSkipReason('workspace-id-invalid');
910
+ return;
911
+ }
912
+ if (cloudAuthConfigured && (!authSessionResolved || !isAuthenticated)) {
913
+ setWorkspaceSyncAiProfilesLastSkipReason(!authSessionResolved ? 'auth-unresolved' : 'auth-required');
914
+ return;
915
+ }
916
+ setWorkspaceSyncAiProfilesLastSkipReason(null);
917
+ try {
918
+ const res = await fetch(`/api/taskforce/sync/workspace/ai-profiles?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
919
+ method: 'GET',
920
+ credentials: 'include'
446
921
  });
922
+ if (!res.ok) {
923
+ setWorkspaceSyncAiProfilesLastFetchError(`HTTP ${res.status}`);
924
+ setWorkspaceSyncAiProfilesLastFetchAt(new Date().toISOString());
925
+ return;
926
+ }
927
+ const data = await res.json().catch(() => ({}));
928
+ const rawAiProfiles = Array.isArray(data?.aiProfiles) ? data.aiProfiles : [];
929
+ setWorkspaceSyncAiProfilesRawCount(rawAiProfiles.length);
930
+ const aiProfiles = Array.isArray(data?.aiProfiles)
931
+ ? data.aiProfiles
932
+ .map((entry) => normalizeWorkspaceSyncAiProfileSnapshotEntry(entry))
933
+ .filter((entry) => entry.id.length > 0 && entry.workspaceId.length > 0)
934
+ : [];
935
+ setWorkspaceSyncAiProfilesLastFetchError(null);
936
+ setWorkspaceSyncAiProfilesLastFetchAt(new Date().toISOString());
937
+ const fingerprint = typeof data?.fingerprint === 'string'
938
+ ? data.fingerprint
939
+ : JSON.stringify(aiProfiles.map((entry) => `${entry.id}:${entry.updatedAt}:${entry.name}:${entry.username}`).sort());
940
+ if (fingerprint === workspaceSyncAiProfilesFingerprintRef.current)
941
+ return;
942
+ workspaceSyncAiProfilesFingerprintRef.current = fingerprint;
943
+ workspaceSyncAiProfilesRef.current = aiProfiles;
944
+ setWorkspaceSyncAiProfiles(aiProfiles);
945
+ setWorkspaceSyncAiProfilesFingerprint(fingerprint);
946
+ }
947
+ catch (error) {
948
+ setWorkspaceSyncAiProfilesLastFetchError(String(error?.message || error || 'unknown-error'));
949
+ setWorkspaceSyncAiProfilesLastFetchAt(new Date().toISOString());
950
+ logSyncDebug('ai_profiles_snapshot_refresh_failed', {
951
+ workspaceId: currentWorkspaceId,
952
+ error: String(error?.message || error || '')
953
+ });
954
+ }
955
+ finally {
956
+ markWorkspaceSyncReferenceSnapshotReady('aiProfiles');
447
957
  }
448
- }, [runtimeMode, currentWorkspaceId, reportSyncEvent]);
958
+ }, [runtimeMode, currentWorkspaceId, cloudAuthConfigured, authSessionResolved, isAuthenticated, markWorkspaceSyncReferenceSnapshotReady]);
959
+ const refreshWorkspaceSyncAssetsSnapshot = useCallback(async () => {
960
+ if (runtimeMode !== 'local')
961
+ return;
962
+ if (!isValidWorkspaceKey(currentWorkspaceId))
963
+ return;
964
+ if (cloudAuthConfigured && (!authSessionResolved || !isAuthenticated))
965
+ return;
966
+ try {
967
+ const res = await fetch(`/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
968
+ method: 'GET',
969
+ credentials: 'include'
970
+ });
971
+ if (!res.ok)
972
+ return;
973
+ const data = await res.json().catch(() => ({}));
974
+ const assets = Array.isArray(data?.assets)
975
+ ? data.assets
976
+ .map((entry) => ({
977
+ path: String(entry?.path || '').trim(),
978
+ updatedAt: String(entry?.updatedAt || '').trim(),
979
+ contentBase64: typeof entry?.contentBase64 === 'string' ? entry.contentBase64 : '',
980
+ assetId: typeof entry?.assetId === 'string' && entry.assetId.trim().length > 0 ? entry.assetId.trim() : undefined,
981
+ kind: entry?.kind === 'image' ? 'image' : 'file',
982
+ mimeType: typeof entry?.mimeType === 'string' && entry.mimeType.trim().length > 0 ? entry.mimeType.trim() : 'application/octet-stream',
983
+ referenceNumber: Number.isFinite(Number(entry?.referenceNumber)) ? Math.max(1, Math.floor(Number(entry.referenceNumber))) : null,
984
+ taskId: typeof entry?.taskId === 'string' && entry.taskId.trim().length > 0 ? entry.taskId.trim() : null,
985
+ logicalName: typeof entry?.logicalName === 'string' && entry.logicalName.trim().length > 0 ? entry.logicalName.trim() : null,
986
+ caption: typeof entry?.caption === 'string' && entry.caption.trim().length > 0 ? entry.caption.trim() : null,
987
+ originalFilename: typeof entry?.originalFilename === 'string' && entry.originalFilename.trim().length > 0 ? entry.originalFilename.trim() : null,
988
+ linkRole: entry?.linkRole === 'reference'
989
+ ? 'reference'
990
+ : entry?.linkRole === 'image'
991
+ ? 'image'
992
+ : 'attachment'
993
+ }))
994
+ .filter((entry) => entry.path.length > 0 && entry.contentBase64.length > 0)
995
+ : [];
996
+ const fingerprint = typeof data?.fingerprint === 'string'
997
+ ? data.fingerprint
998
+ : JSON.stringify(assets.map((entry) => `${entry.path}:${entry.updatedAt}:${entry.contentBase64.length}`).sort());
999
+ if (fingerprint === workspaceSyncAssetsFingerprintRef.current)
1000
+ return;
1001
+ workspaceSyncAssetsFingerprintRef.current = fingerprint;
1002
+ workspaceSyncAssetsRef.current = assets;
1003
+ setWorkspaceSyncAssets(assets);
1004
+ setWorkspaceSyncAssetsFingerprint(fingerprint);
1005
+ }
1006
+ catch (error) {
1007
+ logSyncDebug('assets_snapshot_refresh_failed', {
1008
+ workspaceId: currentWorkspaceId,
1009
+ error: String(error?.message || error || '')
1010
+ });
1011
+ }
1012
+ finally {
1013
+ markWorkspaceSyncReferenceSnapshotReady('assets');
1014
+ }
1015
+ }, [runtimeMode, currentWorkspaceId, cloudAuthConfigured, authSessionResolved, isAuthenticated, markWorkspaceSyncReferenceSnapshotReady]);
1016
+ const refreshWorkspaceSyncDocumentReviewSessionsSnapshot = useCallback(async () => {
1017
+ if (runtimeMode !== 'local')
1018
+ return;
1019
+ if (!isValidWorkspaceKey(currentWorkspaceId))
1020
+ return;
1021
+ if (cloudAuthConfigured && (!authSessionResolved || !isAuthenticated))
1022
+ return;
1023
+ try {
1024
+ const res = await fetch(`/api/taskforce/sync/workspace/document-reviews?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
1025
+ method: 'GET',
1026
+ credentials: 'include'
1027
+ });
1028
+ if (!res.ok)
1029
+ return;
1030
+ const data = await res.json().catch(() => ({}));
1031
+ const sessions = Array.isArray(data?.sessions)
1032
+ ? data.sessions
1033
+ .map((entry) => ({
1034
+ id: String(entry?.id || '').trim(),
1035
+ assetId: String(entry?.assetId || '').trim(),
1036
+ documentId: typeof entry?.documentId === 'string' && entry.documentId.trim().length > 0 ? entry.documentId.trim() : null,
1037
+ documentVersion: Number.isFinite(Number(entry?.documentVersion)) ? Math.max(1, Math.floor(Number(entry.documentVersion))) : null,
1038
+ title: typeof entry?.title === 'string' && entry.title.trim().length > 0 ? entry.title.trim() : null,
1039
+ status: entry?.status === 'resolved' ? 'resolved' : 'open',
1040
+ comments: Array.isArray(entry?.comments) ? entry.comments : [],
1041
+ createdByActorId: typeof entry?.createdByActorId === 'string' && entry.createdByActorId.trim().length > 0 ? entry.createdByActorId.trim() : null,
1042
+ updatedByActorId: typeof entry?.updatedByActorId === 'string' && entry.updatedByActorId.trim().length > 0 ? entry.updatedByActorId.trim() : null,
1043
+ createdAt: String(entry?.createdAt || '').trim(),
1044
+ updatedAt: String(entry?.updatedAt || entry?.createdAt || '').trim(),
1045
+ deletedAt: typeof entry?.deletedAt === 'string' && entry.deletedAt.trim().length > 0 ? entry.deletedAt.trim() : null
1046
+ }))
1047
+ .filter((entry) => entry.id.length > 0 && entry.assetId.length > 0)
1048
+ : [];
1049
+ const comments = Array.isArray(data?.comments)
1050
+ ? data.comments
1051
+ .map((entry) => ({
1052
+ id: String(entry?.id || '').trim(),
1053
+ sessionId: String(entry?.sessionId || '').trim(),
1054
+ body: typeof entry?.body === 'string' ? entry.body : '',
1055
+ anchor: entry?.anchor ?? null,
1056
+ order: Number.isFinite(Number(entry?.order)) ? Math.max(0, Math.floor(Number(entry.order))) : 0,
1057
+ authorActorId: typeof entry?.authorActorId === 'string' && entry.authorActorId.trim().length > 0 ? entry.authorActorId.trim() : null,
1058
+ createdAt: String(entry?.createdAt || '').trim(),
1059
+ updatedAt: String(entry?.updatedAt || entry?.createdAt || '').trim(),
1060
+ deletedAt: typeof entry?.deletedAt === 'string' && entry.deletedAt.trim().length > 0 ? entry.deletedAt.trim() : null
1061
+ }))
1062
+ .filter((entry) => entry.id.length > 0 && entry.sessionId.length > 0)
1063
+ : flattenDocumentReviewComments(sessions);
1064
+ const fingerprint = typeof data?.fingerprint === 'string'
1065
+ ? data.fingerprint
1066
+ : JSON.stringify([
1067
+ ...sessions.map((entry) => `${entry.id}:${entry.updatedAt}:${entry.status}:${entry.deletedAt || ''}:${entry.comments.length}`).sort(),
1068
+ ...comments.map((entry) => `${entry.id}:${entry.sessionId}:${entry.updatedAt}:${entry.deletedAt || ''}`).sort()
1069
+ ]);
1070
+ if (fingerprint === workspaceSyncDocumentReviewSessionsFingerprintRef.current)
1071
+ return;
1072
+ workspaceSyncDocumentReviewSessionsFingerprintRef.current = fingerprint;
1073
+ workspaceSyncDocumentReviewSessionsRef.current = sessions;
1074
+ workspaceSyncDocumentReviewCommentsRef.current = comments;
1075
+ setWorkspaceSyncDocumentReviewSessions(sessions);
1076
+ setWorkspaceSyncDocumentReviewSessionsFingerprint(fingerprint);
1077
+ }
1078
+ catch (error) {
1079
+ logSyncDebug('document_review_sessions_snapshot_refresh_failed', {
1080
+ workspaceId: currentWorkspaceId,
1081
+ error: String(error?.message || error || '')
1082
+ });
1083
+ }
1084
+ finally {
1085
+ markWorkspaceSyncReferenceSnapshotReady('documentReviewSessions');
1086
+ }
1087
+ }, [runtimeMode, currentWorkspaceId, cloudAuthConfigured, authSessionResolved, isAuthenticated, markWorkspaceSyncReferenceSnapshotReady]);
1088
+ const refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot = useCallback(async () => {
1089
+ if (runtimeMode !== 'local')
1090
+ return;
1091
+ if (!isValidWorkspaceKey(currentWorkspaceId))
1092
+ return;
1093
+ if (cloudAuthConfigured && (!authSessionResolved || !isAuthenticated))
1094
+ return;
1095
+ try {
1096
+ const res = await fetch(`/api/taskforce/sync/workspace/annotated-attachments?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
1097
+ method: 'GET',
1098
+ credentials: 'include'
1099
+ });
1100
+ if (!res.ok)
1101
+ return;
1102
+ const data = await res.json().catch(() => ({}));
1103
+ const sessions = Array.isArray(data?.sessions)
1104
+ ? data.sessions
1105
+ .map((entry) => ({
1106
+ id: String(entry?.id || '').trim(),
1107
+ workspaceId: String(entry?.workspaceId || '').trim(),
1108
+ taskId: String(entry?.taskId || '').trim(),
1109
+ baseImageAssetId: String(entry?.baseImageAssetId || '').trim(),
1110
+ title: typeof entry?.title === 'string' && entry.title.trim().length > 0 ? entry.title.trim() : null,
1111
+ globalInstruction: typeof entry?.globalInstruction === 'string' && entry.globalInstruction.trim().length > 0 ? entry.globalInstruction.trim() : null,
1112
+ annotations: Array.isArray(entry?.annotations) ? entry.annotations : [],
1113
+ createdByActorId: typeof entry?.createdByActorId === 'string' && entry.createdByActorId.trim().length > 0 ? entry.createdByActorId.trim() : null,
1114
+ updatedByActorId: typeof entry?.updatedByActorId === 'string' && entry.updatedByActorId.trim().length > 0 ? entry.updatedByActorId.trim() : null,
1115
+ createdAt: String(entry?.createdAt || '').trim(),
1116
+ updatedAt: String(entry?.updatedAt || entry?.createdAt || '').trim(),
1117
+ deletedAt: typeof entry?.deletedAt === 'string' && entry.deletedAt.trim().length > 0 ? entry.deletedAt.trim() : null
1118
+ }))
1119
+ .filter((entry) => (entry.id.length > 0
1120
+ && entry.workspaceId.length > 0
1121
+ && entry.taskId.length > 0
1122
+ && entry.baseImageAssetId.length > 0))
1123
+ : [];
1124
+ const fingerprint = typeof data?.fingerprint === 'string'
1125
+ ? data.fingerprint
1126
+ : JSON.stringify(sessions.map((entry) => `${entry.id}:${entry.updatedAt}:${entry.taskId}:${entry.baseImageAssetId}:${entry.annotations.length}:${entry.deletedAt || ''}`).sort());
1127
+ if (fingerprint === workspaceSyncAnnotatedAttachmentSessionsFingerprintRef.current)
1128
+ return;
1129
+ workspaceSyncAnnotatedAttachmentSessionsFingerprintRef.current = fingerprint;
1130
+ workspaceSyncAnnotatedAttachmentSessionsRef.current = sessions;
1131
+ setWorkspaceSyncAnnotatedAttachmentSessions(sessions);
1132
+ setWorkspaceSyncAnnotatedAttachmentSessionsFingerprint(fingerprint);
1133
+ }
1134
+ catch (error) {
1135
+ logSyncDebug('annotated_attachment_sessions_snapshot_refresh_failed', {
1136
+ workspaceId: currentWorkspaceId,
1137
+ error: String(error?.message || error || '')
1138
+ });
1139
+ }
1140
+ finally {
1141
+ markWorkspaceSyncReferenceSnapshotReady('annotatedAttachmentSessions');
1142
+ }
1143
+ }, [runtimeMode, currentWorkspaceId, cloudAuthConfigured, authSessionResolved, isAuthenticated, markWorkspaceSyncReferenceSnapshotReady]);
1144
+ const seedWorkspaceSnapshotPushBaseline = useCallback(() => {
1145
+ workspaceLastPushedDocumentPathsRef.current = new Set(workspaceSyncMarkdownDocumentsRef.current
1146
+ .map((entry) => String(entry?.path || '').trim())
1147
+ .filter((path) => path.length > 0));
1148
+ workspaceLastPushedDocumentWatermarksRef.current = new Map(workspaceSyncMarkdownDocumentsRef.current
1149
+ .map((entry) => [String(entry?.path || '').trim(), String(entry?.updatedAt || '').trim()])
1150
+ .filter(([path]) => path.length > 0));
1151
+ workspaceLastPushedAssetPathsRef.current = new Set(workspaceSyncAssetsRef.current
1152
+ .map((entry) => String(entry?.path || '').trim())
1153
+ .filter((path) => path.length > 0));
1154
+ workspaceLastPushedAssetWatermarksRef.current = new Map(workspaceSyncAssetsRef.current
1155
+ .map((entry) => [String(entry?.path || '').trim(), String(entry?.updatedAt || '').trim()])
1156
+ .filter(([path]) => path.length > 0));
1157
+ workspaceLastPushedDocumentReviewSessionIdsRef.current = new Set(workspaceSyncDocumentReviewSessionsRef.current
1158
+ .map((entry) => String(entry?.id || '').trim())
1159
+ .filter((id) => id.length > 0));
1160
+ workspaceLastPushedDocumentReviewSessionWatermarksRef.current = new Map(workspaceSyncDocumentReviewSessionsRef.current
1161
+ .map((entry) => [String(entry?.id || '').trim(), String(entry?.updatedAt || entry?.createdAt || '').trim()])
1162
+ .filter(([id]) => id.length > 0));
1163
+ workspaceLastPushedDocumentReviewCommentIdsRef.current = new Set(workspaceSyncDocumentReviewCommentsRef.current
1164
+ .map((entry) => String(entry?.id || '').trim())
1165
+ .filter((id) => id.length > 0));
1166
+ workspaceLastPushedDocumentReviewCommentWatermarksRef.current = new Map(workspaceSyncDocumentReviewCommentsRef.current
1167
+ .map((entry) => [String(entry?.id || '').trim(), String(entry?.updatedAt || entry?.createdAt || '').trim()])
1168
+ .filter(([id]) => id.length > 0));
1169
+ workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set(workspaceSyncAnnotatedAttachmentSessionsRef.current
1170
+ .map((entry) => String(entry?.id || '').trim())
1171
+ .filter((id) => id.length > 0));
1172
+ workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map(workspaceSyncAnnotatedAttachmentSessionsRef.current
1173
+ .map((entry) => [String(entry?.id || '').trim(), String(entry?.updatedAt || entry?.createdAt || '').trim()])
1174
+ .filter(([id]) => id.length > 0));
1175
+ }, []);
1176
+ const refreshLocalWorkspaceTaskCollections = useCallback(async () => {
1177
+ const [tasksRes, archiveRes] = await Promise.all([
1178
+ fetch('/api/taskforce/tasks', {
1179
+ method: 'GET',
1180
+ credentials: 'include'
1181
+ }),
1182
+ fetch('/api/taskforce/archive', {
1183
+ method: 'GET',
1184
+ credentials: 'include'
1185
+ })
1186
+ ]);
1187
+ if (tasksRes.ok) {
1188
+ const tasksPayload = await tasksRes.json().catch(() => ({}));
1189
+ const normalizedTasks = Array.isArray(tasksPayload?.tasks)
1190
+ ? tasksPayload.tasks.map((task) => normalizeTaskFromApi(task))
1191
+ : [];
1192
+ setTasks(normalizedTasks);
1193
+ }
1194
+ if (archiveRes.ok) {
1195
+ const archivePayload = await archiveRes.json().catch(() => ({}));
1196
+ const normalizedArchive = Array.isArray(archivePayload?.archived)
1197
+ ? archivePayload.archived.map((task) => ({
1198
+ ...normalizeTaskFromApi(task),
1199
+ isArchived: true
1200
+ }))
1201
+ : [];
1202
+ setArchivedTasks(normalizedArchive);
1203
+ }
1204
+ await fetchPlanningEntities().catch(() => {
1205
+ logSyncDebug('refresh_planning_entities_failed', {
1206
+ workspaceId: currentWorkspaceId
1207
+ });
1208
+ });
1209
+ }, [currentWorkspaceId, fetchPlanningEntities, setArchivedTasks, setTasks]);
1210
+ // === SECTION: Auth failure handling & sync readiness ===
449
1211
  const handleSyncAuthFailure = useCallback(async (source, statusCode) => {
450
1212
  clearWorkspaceRetry();
451
- logSyncDebug('sync_auth_failure', { source, status: statusCode, workspaceId: currentWorkspaceId });
452
- incrementWorkspaceSyncAuthFailure();
453
- recordWorkspaceSyncFailure(source, statusCode);
1213
+ setWorkspaceSyncBusy(false);
454
1214
  setUserGlobalSyncStatus('error');
455
1215
  const message = 'Session expired. Sign in again to resume cloud sync.';
456
1216
  setUserGlobalSyncError(message);
457
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
+ });
458
1225
  setAuthBlocked(true);
459
1226
  setIsAuthenticated(false);
460
1227
  await checkAuthSession();
461
- }, [clearWorkspaceRetry, currentWorkspaceId, checkAuthSession, recordWorkspaceSyncFailure, incrementWorkspaceSyncAuthFailure, setAuthBlocked, setIsAuthenticated]);
1228
+ }, [
1229
+ clearWorkspaceRetry,
1230
+ reportSyncEvent,
1231
+ currentWorkspaceId,
1232
+ setAuthBlocked,
1233
+ setIsAuthenticated,
1234
+ checkAuthSession
1235
+ ]);
462
1236
  const ensureWorkspaceSyncReady = useCallback(async () => {
463
- const workspaceKey = String(currentWorkspaceId || '').trim();
464
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default') {
465
- logSyncDebug('sync_ready_blocked_unresolved_workspace', { workspaceKey, runtimeMode });
466
- setUserGlobalSyncStatus('error');
1237
+ if (!isValidWorkspaceKey(currentWorkspaceId)) {
467
1238
  const message = 'Workspace sync blocked: local workspace ID is unresolved.';
468
- setUserGlobalSyncError(message);
469
1239
  setWorkspaceLastErrorMessage(message);
470
1240
  setWorkspaceLastErrorAt(new Date().toISOString());
471
- setWorkspaceSyncReady(false);
1241
+ setUserGlobalSyncError(message);
1242
+ setUserGlobalSyncStatus('error');
472
1243
  return false;
473
1244
  }
474
- if (workspaceSyncReadyRef.current.has(workspaceKey))
475
- return true;
476
1245
  const ensured = await ensureCloudWorkspaceReadyForSync();
477
- if (!ensured.success) {
478
- logSyncDebug('sync_ready_failed', { workspaceKey, statusCode: ensured.statusCode || null, error: ensured.error || null });
479
- if (ensured.statusCode === 401) {
480
- await handleSyncAuthFailure('handshake', 401);
481
- return false;
482
- }
483
- if (ensured.statusCode === 429) {
484
- recordWorkspaceSyncFailure('handshake', 429);
485
- setUserGlobalSyncStatus('error');
486
- const message = ensured.error || 'Sync paused — rate limit reached. Will retry automatically.';
487
- setUserGlobalSyncError(message);
488
- setWorkspaceLastErrorMessage(message);
489
- setWorkspaceLastErrorAt(new Date().toISOString());
490
- setWorkspaceSyncReady(false);
491
- scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
492
- minDelayMs: ensured.retryAfterMs,
493
- workspaceId: workspaceKey
494
- });
495
- return false;
496
- }
497
- recordWorkspaceSyncFailure('handshake');
498
- setUserGlobalSyncStatus('error');
499
- const message = ensured.error || 'Workspace sync handshake failed.';
500
- setUserGlobalSyncError(message);
501
- setWorkspaceLastErrorMessage(message);
502
- setWorkspaceSyncReady(false);
1246
+ if (ensured.success)
1247
+ return true;
1248
+ if (ensured.statusCode === 401) {
1249
+ await handleSyncAuthFailure('handshake', 401);
503
1250
  return false;
504
1251
  }
505
- logSyncDebug('sync_ready_ok', { workspaceKey });
506
- workspaceSyncReadyRef.current.add(workspaceKey);
507
- setWorkspaceSyncReady(true);
508
- return true;
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;
509
1270
  }, [
510
1271
  currentWorkspaceId,
511
1272
  ensureCloudWorkspaceReadyForSync,
512
- runtimeMode,
513
- recordWorkspaceSyncFailure,
514
1273
  handleSyncAuthFailure,
515
- scheduleWorkspacePullRetry
1274
+ reportSyncEvent,
1275
+ scheduleWorkspaceSyncRetry,
1276
+ workspaceSyncPhase,
1277
+ persistWorkspaceSyncPatchSafely
516
1278
  ]);
517
- const pushWorkspaceChangesToCloud = useCallback(async (signature) => {
1279
+ // === SECTION: Push sync ===
1280
+ const pushWorkspaceChangesToCloud = useCallback(async (signature, options) => {
518
1281
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
519
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
+ }
520
1293
  if (!isAuthenticated) {
521
1294
  const authed = await checkAuthSession();
522
1295
  if (!authed)
523
1296
  return false;
524
1297
  }
525
- const workspaceKey = String(currentWorkspaceId || '').trim();
526
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
1298
+ if (shouldSuppressAttachBootstrapPush(signature)) {
527
1299
  return false;
528
- if (workspaceSyncSourceOfTruth === 'cloud' && !workspaceCloudHydratedRef.current.has(workspaceKey)) {
1300
+ }
1301
+ if (workspacePushInFlightRef.current || workspacePullInFlightRef.current) {
529
1302
  workspacePendingSignatureRef.current = signature;
530
- const now = Date.now();
531
- const blockedAt = workspacePushBlockedSinceRef.current.get(workspaceKey) || now;
532
- workspacePushBlockedSinceRef.current.set(workspaceKey, blockedAt);
533
- setWorkspaceLastWarningMessage(WORKSPACE_HYDRATION_PUSH_BLOCK_WARNING);
534
- if (isWorkspacePullRetryPending()) {
535
- const blockedMs = now - blockedAt;
536
- const nextRecoveryAt = workspacePushBlockedRecoveryAtRef.current.get(workspaceKey) || 0;
537
- if (blockedMs >= WORKSPACE_HYDRATION_PUSH_BLOCK_ESCAPE_MS && now >= nextRecoveryAt) {
538
- workspacePushBlockedRecoveryAtRef.current.set(workspaceKey, now + WORKSPACE_HYDRATION_PUSH_BLOCK_RECOVERY_COOLDOWN_MS);
539
- logSyncDebug('push_blocked_waiting_hydration_force_pull', {
540
- workspaceId: workspaceKey,
541
- blockedMs
542
- });
543
- clearWorkspaceRetry();
544
- window.setTimeout(() => {
545
- void pullFromCloudRef.current();
546
- }, 120);
547
- }
548
- }
549
1303
  return false;
550
1304
  }
551
- if (workspacePushInFlightRef.current) {
1305
+ const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'push');
1306
+ if (!leaseAcquired) {
552
1307
  workspacePendingSignatureRef.current = signature;
1308
+ reportWorkspaceWindowContention('push');
553
1309
  return false;
554
1310
  }
555
1311
  workspacePushInFlightRef.current = true;
1312
+ setWorkspaceSyncBusy(true);
556
1313
  setUserGlobalSyncStatus('syncing');
557
1314
  setUserGlobalSyncError(null);
558
1315
  const pushStartedAtMs = Date.now();
1316
+ const forceAllAiProfiles = options?.forceAllAiProfiles === true || workspaceForceFullAiProfilePushRef.current === true;
559
1317
  try {
560
- await refreshWorkspaceSyncDocumentsSnapshot();
1318
+ await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1319
+ await refreshWorkspaceSyncAiProfilesSnapshot();
1320
+ await refreshWorkspaceSyncAssetsSnapshot();
1321
+ await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
561
1322
  const ready = await ensureWorkspaceSyncReady();
562
1323
  if (!ready)
563
1324
  return false;
564
- const payload = buildWorkspaceSyncPayload();
565
- logSyncDebug('push_start', {
566
- workspaceId: currentWorkspaceId,
567
- changeCount: payload.changes.length,
568
- deleteCount: payload.deleteTaskIds.size
569
- });
570
- setUserGlobalSyncPendingChanges(payload.changes.length);
1325
+ const payload = buildWorkspaceSyncPayload({ forceAllAiProfiles });
1326
+ setWorkspaceSyncPendingChanges(payload.changes.length);
571
1327
  const pushResult = await runWorkspacePushSyncService({
572
1328
  resolveCloudAuthUrl,
573
1329
  workspaceId: currentWorkspaceId,
574
1330
  payload,
575
- ensureCloudWorkspaceReadyForSync
1331
+ ensureCloudWorkspaceReadyForSync,
1332
+ repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true
576
1333
  });
577
1334
  if (!pushResult.success) {
578
1335
  if (pushResult.status === 401) {
579
1336
  await handleSyncAuthFailure('push', 401);
580
1337
  return false;
581
1338
  }
582
- logSyncDebug('push_failed_http', {
583
- workspaceId: currentWorkspaceId,
584
- status: pushResult.status || 0,
585
- error: pushResult.error || null,
586
- code: pushResult.code || null,
587
- elapsedMs: Date.now() - pushStartedAtMs,
588
- requestMs: pushResult.requestMs
589
- });
590
- recordWorkspaceSyncFailure('push', pushResult.status || undefined);
591
- setUserGlobalSyncStatus('error');
592
- const message = pushResult.error
1339
+ let message = pushResult.error
593
1340
  ? `Workspace sync push failed (${pushResult.status || 0}): ${pushResult.error}`
594
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');
595
1346
  setUserGlobalSyncError(message);
596
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
+ }
597
1363
  return false;
598
1364
  }
599
1365
  workspaceLastPushedSignatureRef.current = signature;
600
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);
601
1373
  workspaceLastPushedDocumentPathsRef.current = new Set(pushResult.currentDocumentPaths);
602
1374
  workspaceLastPushedDocumentWatermarksRef.current = new Map(pushResult.currentDocumentWatermarks);
603
- // Merge newly pushed watermarks into the per-task map so subsequent
604
- // pushes skip tasks that haven't changed since this push.
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
+ }
605
1386
  for (const [taskId, watermark] of pushResult.pushedWatermarks) {
606
1387
  workspaceLastPushedWatermarksRef.current.set(taskId, watermark);
607
1388
  }
@@ -611,28 +1392,22 @@ export function useSyncOrchestrator(input) {
611
1392
  workspaceLastPushedWatermarksRef.current.delete(taskId);
612
1393
  }
613
1394
  }
614
- const syncedAt = pushResult.syncedAt || new Date().toISOString();
615
- logSyncDebug('push_success', {
616
- workspaceId: currentWorkspaceId,
617
- syncedAt,
618
- changeCount: pushResult.changeCount,
619
- deleteCount: pushResult.deleteCount,
620
- elapsedMs: Date.now() - pushStartedAtMs,
621
- requestMs: pushResult.requestMs
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
622
1406
  });
623
- setUserGlobalLastSyncedAt(syncedAt);
624
- setWorkspaceLastPushAt(syncedAt);
625
- setUserGlobalSyncPendingChanges(0);
626
- setUserGlobalSyncStatus('idle');
627
- setUserGlobalSyncError(null);
628
- setWorkspaceLastErrorMessage(null);
629
- setWorkspaceLastErrorAt(null);
630
1407
  if (Array.isArray(pushResult.emittedEventIds) && pushResult.emittedEventIds.length > 0) {
631
1408
  for (const rawEventId of pushResult.emittedEventIds) {
632
1409
  const eventId = String(rawEventId || '').trim();
633
- if (!eventId)
634
- continue;
635
- if (realtimeSuppressedEventIdsRef.current.has(eventId))
1410
+ if (!eventId || realtimeSuppressedEventIdsRef.current.has(eventId))
636
1411
  continue;
637
1412
  realtimeSuppressedEventIdsRef.current.add(eventId);
638
1413
  realtimeSuppressedEventQueueRef.current.push(eventId);
@@ -643,25 +1418,53 @@ export function useSyncOrchestrator(input) {
643
1418
  realtimeSuppressedEventIdsRef.current.delete(stale);
644
1419
  }
645
1420
  }
646
- void persistWorkspaceSyncPatch({
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',
647
1429
  lastPushAt: syncedAt,
648
- lastSyncedAt: 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
649
1438
  });
650
1439
  return true;
651
1440
  }
652
- catch {
653
- logSyncDebug('push_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pushStartedAtMs });
654
- recordWorkspaceSyncFailure('push');
655
- setUserGlobalSyncStatus('error');
1441
+ catch (error) {
656
1442
  const message = 'Workspace sync push failed.';
1443
+ setUserGlobalSyncStatus('error');
657
1444
  setUserGlobalSyncError(message);
658
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
+ });
659
1458
  return false;
660
1459
  }
661
1460
  finally {
662
1461
  workspacePushInFlightRef.current = false;
1462
+ setWorkspaceSyncBusy(workspacePullInFlightRef.current);
1463
+ releaseWorkspaceSyncLease(currentWorkspaceId);
663
1464
  const pendingSignature = workspacePendingSignatureRef.current;
664
- if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
1465
+ if (pendingSignature
1466
+ && pendingSignature !== workspaceLastPushedSignatureRef.current
1467
+ && !workspacePullInFlightRef.current) {
665
1468
  workspacePendingSignatureRef.current = '';
666
1469
  window.setTimeout(() => {
667
1470
  void pushWorkspaceChangesToCloud(pendingSignature);
@@ -671,171 +1474,124 @@ export function useSyncOrchestrator(input) {
671
1474
  }, [
672
1475
  cloudAuthConfigured,
673
1476
  runtimeMode,
1477
+ workspaceCloudSyncEnabled,
1478
+ currentWorkspaceId,
1479
+ workspaceSyncPhase,
674
1480
  isAuthenticated,
675
1481
  checkAuthSession,
676
- workspaceCloudSyncEnabled,
677
- workspaceSyncSourceOfTruth,
678
- buildWorkspaceSyncPayload,
1482
+ clearWorkspaceIssue,
1483
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1484
+ refreshWorkspaceSyncAiProfilesSnapshot,
1485
+ refreshWorkspaceSyncAssetsSnapshot,
1486
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
679
1487
  ensureWorkspaceSyncReady,
680
- refreshWorkspaceSyncDocumentsSnapshot,
1488
+ buildWorkspaceSyncPayload,
1489
+ resolveCloudAuthUrl,
681
1490
  ensureCloudWorkspaceReadyForSync,
1491
+ shouldSuppressAttachBootstrapPush,
682
1492
  handleSyncAuthFailure,
683
- recordWorkspaceSyncFailure,
684
1493
  clearWorkspaceRetry,
685
- isWorkspacePullRetryPending,
686
- currentWorkspaceId,
687
- resolveCloudAuthUrl
1494
+ persistWorkspaceSyncPatch,
1495
+ reportSyncEvent,
1496
+ scheduleWorkspaceSyncRetry,
1497
+ acquireWorkspaceSyncLease,
1498
+ releaseWorkspaceSyncLease,
1499
+ reportWorkspaceWindowContention,
1500
+ workspacePushInFlightRef,
1501
+ workspacePullInFlightRef,
1502
+ persistWorkspaceSyncPatchSafely
688
1503
  ]);
689
- const pullWorkspaceChangesFromCloud = useCallback(async () => {
690
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled) {
691
- logSyncDebug('pull_abort', { reason: 'config_disabled', cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled });
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())
692
1515
  return false;
693
- }
694
1516
  if (!isAuthenticated) {
695
1517
  const authed = await checkAuthSession();
696
- if (!authed) {
697
- logSyncDebug('pull_abort', { reason: 'not_authenticated' });
1518
+ if (!authed)
698
1519
  return false;
699
- }
700
- }
701
- const workspaceKey = String(currentWorkspaceId || '').trim();
702
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default') {
703
- logSyncDebug('pull_abort', { reason: 'invalid_workspace', workspaceKey });
704
- return false;
705
- }
706
- if (isWorkspacePullRetryPending()) {
707
- logSyncDebug('pull_abort', { reason: 'retry_pending' });
708
- return false;
709
1520
  }
710
- if (workspacePullInFlightRef.current) {
711
- logSyncDebug('pull_abort', { reason: 'already_in_flight' });
1521
+ if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
712
1522
  return false;
713
- }
714
- const leaseAcquired = await tryAcquireWorkspacePullLease(workspaceKey);
1523
+ const leaseAcquired = await acquireWorkspaceSyncLease(currentWorkspaceId, 'pull');
715
1524
  if (!leaseAcquired) {
716
- markWorkspacePullCooldown(workspaceKey, 12_000);
717
- logSyncDebug('pull_abort', { reason: 'non_leader_tab', workspaceId: workspaceKey });
1525
+ reportWorkspaceWindowContention('pull');
718
1526
  return false;
719
1527
  }
720
1528
  workspacePullInFlightRef.current = true;
721
- const bootstrapThisRun = !workspaceCloudHydratedRef.current.has(workspaceKey);
722
- if (bootstrapThisRun) {
723
- setWorkspaceBootstrapSyncInProgress(true);
724
- setWorkspaceBootstrapSyncPages(0);
725
- setWorkspaceBootstrapSyncAppliedChanges(0);
726
- }
1529
+ setWorkspaceSyncBusy(true);
727
1530
  setUserGlobalSyncStatus('syncing');
728
1531
  setUserGlobalSyncError(null);
729
1532
  const pullStartedAtMs = Date.now();
730
1533
  try {
731
1534
  const ready = await ensureWorkspaceSyncReady();
732
- if (!ready) {
733
- logSyncDebug('pull_abort', { reason: 'ensure_ready_failed' });
1535
+ if (!ready)
734
1536
  return false;
735
- }
736
- const initialCursor = workspacePullBootstrapDoneRef.current
737
- ? workspacePullCursorRef.current
738
- : null;
739
- logSyncDebug('pull_start', { workspaceId: currentWorkspaceId, cursor: initialCursor });
1537
+ const bootstrap = workspaceSyncPhase === 'attach-cloud';
1538
+ const cursor = bootstrap ? null : workspacePullCursorRef.current;
740
1539
  const pullResult = await runWorkspacePullSyncService({
741
1540
  resolveCloudAuthUrl,
742
1541
  workspaceId: currentWorkspaceId,
743
- cursor: initialCursor,
744
- bootstrap: bootstrapThisRun,
1542
+ cursor,
1543
+ bootstrap,
745
1544
  ensureCloudWorkspaceReadyForSync,
746
1545
  maxPages: 20,
747
- onPagePulled: bootstrapThisRun ? () => {
748
- setWorkspaceBootstrapSyncPages((prev) => prev + 1);
749
- } : undefined,
750
- onChangesApplied: bootstrapThisRun ? (count) => {
751
- setWorkspaceBootstrapSyncAppliedChanges((prev) => prev + count);
752
- } : undefined
1546
+ repairMode: options?.repairMode === true || workspaceRepairModeRef.current === true,
1547
+ onPagePulled: () => { bootstrapLastProgressAtRef.current = Date.now(); },
1548
+ onChangesApplied: () => { bootstrapLastProgressAtRef.current = Date.now(); }
753
1549
  });
754
1550
  if (!pullResult.success) {
755
- if (pullResult.kind === 'pull_http') {
756
- if (pullResult.status === 401) {
757
- await handleSyncAuthFailure('pull', 401);
758
- return false;
759
- }
760
- logSyncDebug('pull_failed_http', {
761
- workspaceId: currentWorkspaceId,
762
- status: pullResult.status || 0,
763
- cursor: pullResult.cursor,
764
- elapsedMs: Date.now() - pullStartedAtMs,
765
- requestMs: pullResult.pullRequestMs,
766
- applyMs: pullResult.applyMs
767
- });
768
- reportSyncEvent(currentWorkspaceId, {
769
- eventType: 'pull',
770
- status: 'error',
771
- statusCode: pullResult.status || 0,
772
- requestMs: pullResult.pullRequestMs
773
- });
774
- recordWorkspaceSyncFailure('pull', pullResult.status || undefined);
775
- setUserGlobalSyncStatus('error');
776
- const message = `Workspace sync pull failed (${pullResult.status || 0})`;
777
- setUserGlobalSyncError(message);
778
- setWorkspaceLastErrorMessage(message);
779
- if ((pullResult.status || 0) === 429 || (pullResult.status || 0) >= 500) {
780
- scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
781
- minDelayMs: pullResult.retryAfterMs,
782
- workspaceId: workspaceKey
783
- });
784
- }
785
- else {
786
- clearWorkspaceRetry();
787
- }
1551
+ if (pullResult.kind === 'pull_http' && pullResult.status === 401) {
1552
+ await handleSyncAuthFailure('pull', 401);
788
1553
  return false;
789
1554
  }
790
1555
  if (pullResult.kind === 'apply_auth') {
791
1556
  await handleSyncAuthFailure('apply', 401);
792
1557
  return false;
793
1558
  }
794
- if (pullResult.kind === 'apply_all_candidates') {
795
- const applyFailures = pullResult.failures || [];
796
- logSyncDebug('apply_failed_all_candidates', {
797
- workspaceId: currentWorkspaceId,
798
- failures: applyFailures
799
- });
800
- setUserGlobalSyncStatus('error');
801
- const detail = applyFailures.length > 0 ? ` [${applyFailures.join(', ')}]` : '';
802
- recordWorkspaceSyncFailure('apply');
803
- const message = `Workspace sync apply failed${detail}`;
804
- setUserGlobalSyncError(message);
805
- setWorkspaceLastErrorMessage(message);
806
- if (pullResult.hasTransientApplyFailure) {
807
- scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
808
- workspaceId: workspaceKey
809
- });
810
- }
811
- else {
812
- clearWorkspaceRetry();
813
- }
814
- return false;
815
- }
816
- if (pullResult.kind === 'apply_payload') {
817
- logSyncDebug('apply_failed_payload', {
818
- workspaceId: currentWorkspaceId,
819
- error: String(pullResult.error || '')
820
- });
821
- setUserGlobalSyncStatus('error');
822
- recordWorkspaceSyncFailure('apply');
823
- const message = String(pullResult.error || 'Workspace sync apply failed.');
824
- setUserGlobalSyncError(message);
825
- setWorkspaceLastErrorMessage(message);
826
- return false;
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.';
827
1564
  }
828
- logSyncDebug('pull_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pullStartedAtMs });
829
- recordWorkspaceSyncFailure('pull');
830
1565
  setUserGlobalSyncStatus('error');
831
- const message = 'Workspace sync pull failed.';
832
1566
  setUserGlobalSyncError(message);
833
1567
  setWorkspaceLastErrorMessage(message);
834
- scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
835
- workspaceId: workspaceKey
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
836
1575
  });
1576
+ if (pullResult.transient || pullResult.hasTransientApplyFailure || isTransientWorkspaceSyncStatus(pullResult.status)) {
1577
+ scheduleWorkspaceSyncRetry(pullResult.retryAfterMs);
1578
+ }
1579
+ else {
1580
+ persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
1581
+ }
837
1582
  return false;
838
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
+ }
839
1595
  if (pullResult.pulledDeleteTaskIds.size > 0) {
840
1596
  setTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
841
1597
  setArchivedTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
@@ -844,117 +1600,55 @@ export function useSyncOrchestrator(input) {
844
1600
  ? pullResult.documentDecryptFailures.filter((value) => String(value || '').trim().length > 0)
845
1601
  : [];
846
1602
  if (decryptFailures.length > 0) {
847
- const warning = `Some synced documents could not be decrypted (${decryptFailures.length}).`;
848
- setWorkspaceLastWarningMessage(warning);
849
- logSyncDebug('pull_document_decrypt_partial_failure', {
1603
+ logSyncDebug('pull_document_decrypt_failures', {
850
1604
  workspaceId: currentWorkspaceId,
851
- count: decryptFailures.length,
852
- paths: decryptFailures
853
- });
854
- reportSyncEvent(currentWorkspaceId, {
855
- eventType: 'apply',
856
- status: 'error',
857
- errorMessage: `${warning} paths=${decryptFailures.join(', ')}`,
858
- changeCount: pullResult.appliedChanges
1605
+ failureCount: decryptFailures.length
859
1606
  });
860
1607
  }
861
- else {
862
- setWorkspaceLastWarningMessage(null);
863
- }
864
1608
  workspacePullCursorRef.current = pullResult.cursor || null;
865
- workspacePullBootstrapDoneRef.current = true;
866
- workspaceCloudHydratedRef.current.add(workspaceKey);
867
- workspacePushBlockedSinceRef.current.delete(workspaceKey);
868
- workspacePushBlockedRecoveryAtRef.current.delete(workspaceKey);
869
- setWorkspaceCloudHydrated(true);
870
- // If a local mutation happened before initial cloud hydration finished, flush it now.
871
- const currentSignature = buildWorkspaceSyncSignature();
872
- if (currentSignature && currentSignature !== workspaceLastPushedSignatureRef.current) {
873
- workspacePendingSignatureRef.current = currentSignature;
874
- }
875
1609
  if (pullResult.appliedChanges > 0 && (pullResult.pulledActiveUpserts || pullResult.pulledArchivedUpserts)) {
876
- // Source of truth for UI is local DB after apply-local writes.
877
1610
  try {
878
- const refreshRequests = [];
879
- if (pullResult.pulledActiveUpserts) {
880
- refreshRequests.push(fetch('/api/taskforce/tasks', {
881
- method: 'GET',
882
- credentials: 'include'
883
- }));
884
- }
885
- if (pullResult.pulledArchivedUpserts) {
886
- refreshRequests.push(fetch('/api/taskforce/archive', {
887
- method: 'GET',
888
- credentials: 'include'
889
- }));
890
- }
891
- const refreshResponses = await Promise.all(refreshRequests);
892
- for (const response of refreshResponses) {
893
- if (!response.ok)
894
- continue;
895
- const data = await response.json().catch(() => ({}));
896
- if (Array.isArray(data?.tasks)) {
897
- setTasks(data.tasks);
898
- }
899
- else if (Array.isArray(data?.archived)) {
900
- setArchivedTasks(data.archived);
901
- }
902
- }
1611
+ await refreshLocalWorkspaceTaskCollections();
903
1612
  }
904
1613
  catch {
905
1614
  logSyncDebug('pull_local_refresh_failed', {
906
1615
  workspaceId: currentWorkspaceId,
907
1616
  appliedChanges: pullResult.appliedChanges
908
1617
  });
909
- reportSyncEvent(currentWorkspaceId, {
910
- eventType: 'apply',
911
- status: 'error',
912
- errorMessage: 'Local state refresh failed after apply-local.',
913
- changeCount: pullResult.appliedChanges
914
- });
915
1618
  }
916
1619
  }
1620
+ clearWorkspaceRetry();
1621
+ clearWorkspaceIssue();
917
1622
  const syncedAt = pullResult.syncedAt || new Date().toISOString();
918
- logSyncDebug('pull_success', {
919
- workspaceId: currentWorkspaceId,
920
- syncedAt,
921
- appliedChanges: pullResult.appliedChanges,
922
- pulledDeleteCount: pullResult.pulledDeleteTaskIds.size,
923
- cursor: workspacePullCursorRef.current,
924
- elapsedMs: Date.now() - pullStartedAtMs,
925
- requestMs: pullResult.pullRequestMs,
926
- applyMs: pullResult.applyMs,
927
- pages: pullResult.pages
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
928
1636
  });
929
1637
  reportSyncEvent(currentWorkspaceId, {
930
- eventType: 'pull',
1638
+ eventType: workspaceSyncPhase === 'attach-cloud' ? 'bootstrap' : 'pull',
931
1639
  status: 'success',
932
1640
  changeCount: pullResult.appliedChanges,
933
1641
  requestMs: pullResult.pullRequestMs
934
1642
  });
935
- setUserGlobalLastSyncedAt(syncedAt);
936
- setWorkspaceLastPullAt(syncedAt);
937
- setUserGlobalSyncPendingChanges(0);
938
- setUserGlobalSyncStatus('idle');
939
- setUserGlobalSyncError(null);
940
- setWorkspaceLastErrorMessage(null);
941
- setWorkspaceLastErrorAt(null);
942
- clearWorkspaceRetry();
943
- void persistWorkspaceSyncPatch({
944
- pullCursor: workspacePullCursorRef.current,
945
- lastPullAt: syncedAt,
946
- lastSyncedAt: syncedAt
947
- });
948
- // I6: delay onboardingCompleted until first successful pull-and-apply.
949
- // The setup flow intentionally starts with onboardingCompleted=false so that
950
- // a broken apply-local path re-surfaces the onboarding prompt rather than
951
- // leaving the user silently stuck with stale data.
952
- if (!onboardingCompletedRef.current) {
953
- onboardingCompletedRef.current = true;
954
- setWorkspaceSyncOnboardingCompleted(true);
955
- void persistWorkspaceSyncPatch({ onboardingCompleted: true });
1643
+ if (workspaceSyncPhase === 'attach-cloud') {
1644
+ await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
1645
+ await refreshWorkspaceSyncAssetsSnapshot();
1646
+ await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
1647
+ await refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
1648
+ seedWorkspaceSnapshotPushBaseline();
1649
+ return true;
956
1650
  }
957
- const pendingSignature = workspacePendingSignatureRef.current;
1651
+ const pendingSignature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
958
1652
  if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
959
1653
  workspacePendingSignatureRef.current = '';
960
1654
  window.setTimeout(() => {
@@ -963,122 +1657,116 @@ export function useSyncOrchestrator(input) {
963
1657
  }
964
1658
  return true;
965
1659
  }
966
- catch {
967
- logSyncDebug('pull_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pullStartedAtMs });
968
- recordWorkspaceSyncFailure('pull');
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.';
969
1665
  setUserGlobalSyncStatus('error');
970
- const message = 'Workspace sync pull failed.';
971
1666
  setUserGlobalSyncError(message);
972
1667
  setWorkspaceLastErrorMessage(message);
973
- scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
974
- workspaceId: workspaceKey
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
975
1680
  });
976
1681
  return false;
977
1682
  }
978
1683
  finally {
979
- if (bootstrapThisRun) {
980
- setWorkspaceBootstrapSyncInProgress(false);
981
- }
982
1684
  workspacePullInFlightRef.current = false;
983
- releaseWorkspacePullLease(workspaceKey);
1685
+ setWorkspaceSyncBusy(workspacePushInFlightRef.current);
1686
+ releaseWorkspaceSyncLease(currentWorkspaceId);
984
1687
  }
985
1688
  }, [
986
1689
  cloudAuthConfigured,
987
1690
  runtimeMode,
988
- isAuthenticated,
989
- checkAuthSession,
990
1691
  workspaceCloudSyncEnabled,
991
- resolveCloudAuthUrl,
992
1692
  currentWorkspaceId,
993
- isWorkspacePullRetryPending,
1693
+ workspaceSyncPhase,
1694
+ isWorkspaceRetryPending,
1695
+ isAuthenticated,
1696
+ checkAuthSession,
1697
+ clearWorkspaceIssue,
994
1698
  ensureWorkspaceSyncReady,
1699
+ resolveCloudAuthUrl,
1700
+ ensureCloudWorkspaceReadyForSync,
995
1701
  handleSyncAuthFailure,
996
- recordWorkspaceSyncFailure,
1702
+ refreshLocalWorkspaceTaskCollections,
1703
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
1704
+ refreshWorkspaceSyncAssetsSnapshot,
1705
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
1706
+ refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
997
1707
  clearWorkspaceRetry,
998
- scheduleWorkspacePullRetry,
999
- tryAcquireWorkspacePullLease,
1000
- releaseWorkspacePullLease,
1001
- markWorkspacePullCooldown,
1002
- reportSyncEvent,
1003
1708
  persistWorkspaceSyncPatch,
1004
- buildWorkspaceSyncSignature
1709
+ reportSyncEvent,
1710
+ buildWorkspaceSyncSignature,
1711
+ pushWorkspaceChangesToCloud,
1712
+ seedWorkspaceSnapshotPushBaseline,
1713
+ scheduleWorkspaceSyncRetry,
1714
+ acquireWorkspaceSyncLease,
1715
+ releaseWorkspaceSyncLease,
1716
+ reportWorkspaceWindowContention,
1717
+ workspacePushInFlightRef,
1718
+ workspacePullInFlightRef,
1719
+ persistWorkspaceSyncPatchSafely
1005
1720
  ]);
1006
- // Keep the stable pull ref in sync so ensureWorkspaceSyncReady can schedule
1007
- // a retry callback without creating a forward-reference dependency cycle.
1008
- useEffect(() => {
1009
- pullFromCloudRef.current = pullWorkspaceChangesFromCloud;
1010
- }, [pullWorkspaceChangesFromCloud]);
1011
- // Mirror onboardingCompleted state to a ref so the pull callback can read it
1012
- // without stale-closure risk.
1013
- useEffect(() => {
1014
- onboardingCompletedRef.current = workspaceSyncOnboardingCompleted;
1015
- }, [workspaceSyncOnboardingCompleted]);
1721
+ // === SECTION: Realtime sync integration ===
1016
1722
  const handleRealtimeSignal = useCallback((signal) => {
1017
1723
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1018
1724
  return;
1725
+ if (workspaceSyncPhase === 'provision-local')
1726
+ return;
1019
1727
  const eventId = String(signal?.eventId || '').trim();
1020
- if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId)) {
1021
- logSyncDebug('realtime_signal_suppressed_self_echo', {
1022
- workspaceId: currentWorkspaceId,
1023
- eventId,
1024
- signalType: signal.type
1025
- });
1728
+ if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
1026
1729
  return;
1027
- }
1028
- if (isWorkspacePullRetryPending() || workspacePullInFlightRef.current)
1730
+ if (isWorkspaceRetryPending() || workspacePullInFlightRef.current)
1029
1731
  return;
1030
1732
  if (realtimePullDebounceRef.current !== null) {
1031
1733
  window.clearTimeout(realtimePullDebounceRef.current);
1032
1734
  }
1033
1735
  realtimePullDebounceRef.current = window.setTimeout(() => {
1034
1736
  realtimePullDebounceRef.current = null;
1035
- void pullFromCloudRef.current();
1737
+ void pullWorkspaceChangesFromCloud();
1036
1738
  }, 180);
1037
1739
  }, [
1038
1740
  cloudAuthConfigured,
1039
1741
  runtimeMode,
1040
1742
  workspaceCloudSyncEnabled,
1041
- isWorkspacePullRetryPending,
1743
+ workspaceSyncPhase,
1744
+ isWorkspaceRetryPending,
1042
1745
  workspacePullInFlightRef,
1043
- currentWorkspaceId
1746
+ pullWorkspaceChangesFromCloud
1044
1747
  ]);
1045
1748
  const realtimeSocketUrl = resolveWebSocketUrl('/taskforce-ws');
1749
+ const normalizedRealtimeUserId = String(authUserId || '').trim();
1046
1750
  const realtimeEnabled = Boolean(realtimeSyncEnabled
1047
1751
  && cloudAuthConfigured
1048
1752
  && runtimeMode === 'local'
1049
1753
  && workspaceCloudSyncEnabled
1050
- && isAuthenticated);
1754
+ && authSessionResolved
1755
+ && isAuthenticated
1756
+ && normalizedRealtimeUserId
1757
+ && normalizedRealtimeUserId !== 'anonymous'
1758
+ && realtimeSocketUrl);
1051
1759
  const realtimeSync = useRealtimeSync({
1052
1760
  enabled: realtimeEnabled,
1053
1761
  workspaceId: currentWorkspaceId,
1054
1762
  websocketUrl: realtimeSocketUrl,
1055
1763
  onSignal: handleRealtimeSignal,
1056
- onTelemetry: setRealtimeTelemetry
1764
+ onTelemetry: setRealtimeTelemetry,
1765
+ userId: normalizedRealtimeUserId || undefined
1057
1766
  });
1058
1767
  useEffect(() => {
1059
1768
  setRealtimeConnectionState(realtimeSync.connectionState);
1060
1769
  }, [realtimeSync.connectionState]);
1061
- useEffect(() => {
1062
- const current = realtimeTelemetry;
1063
- const previous = realtimeTelemetrySnapshotRef.current;
1064
- const deltaDuplicates = Math.max(0, current.duplicateDiscarded - previous.duplicateDiscarded);
1065
- const deltaOutOfOrder = Math.max(0, current.outOfOrderDiscarded - previous.outOfOrderDiscarded);
1066
- const deltaInvalid = Math.max(0, current.invalidDiscarded - previous.invalidDiscarded);
1067
- if (deltaDuplicates === 0 && deltaOutOfOrder === 0 && deltaInvalid === 0)
1068
- return;
1069
- const now = Date.now();
1070
- const noisyOnly = deltaOutOfOrder === 0;
1071
- if (noisyOnly && now - realtimeTelemetryReportedAtRef.current < 5000) {
1072
- return;
1073
- }
1074
- realtimeTelemetryReportedAtRef.current = now;
1075
- realtimeTelemetrySnapshotRef.current = { ...current };
1076
- reportSyncEvent(currentWorkspaceId, {
1077
- eventType: 'apply',
1078
- status: deltaOutOfOrder > 0 ? 'error' : 'success',
1079
- errorMessage: `Realtime telemetry duplicate=${deltaDuplicates} outOfOrder=${deltaOutOfOrder} invalid=${deltaInvalid}`
1080
- });
1081
- }, [realtimeTelemetry, reportSyncEvent, currentWorkspaceId]);
1082
1770
  useEffect(() => {
1083
1771
  return () => {
1084
1772
  if (realtimePullDebounceRef.current !== null) {
@@ -1087,6 +1775,7 @@ export function useSyncOrchestrator(input) {
1087
1775
  }
1088
1776
  };
1089
1777
  }, []);
1778
+ // === SECTION: Repair & manual retry ===
1090
1779
  const fetchLocalWorkspaceSnapshot = useCallback(async () => {
1091
1780
  const [localTasksRes, localArchiveRes] = await Promise.all([
1092
1781
  fetch('/api/taskforce/tasks', {
@@ -1112,110 +1801,308 @@ export function useSyncOrchestrator(input) {
1112
1801
  archived: Array.isArray(localArchivePayload?.archived) ? localArchivePayload.archived : []
1113
1802
  };
1114
1803
  }, []);
1115
- const saveWorkspaceCloudSyncSettings = useCallback(async (input) => {
1116
- return saveWorkspaceCloudSyncSettingsBase(input, {
1804
+ const saveWorkspaceCloudSyncSettings = useCallback(async (settings) => {
1805
+ return saveWorkspaceCloudSyncSettingsBase(settings, {
1117
1806
  cloudAuthConfigured,
1118
1807
  isAuthenticated,
1119
- resolveCloudAuthUrl,
1120
- ensureCloudWorkspaceReadyForSync,
1121
- fetchLocalWorkspaceSnapshot
1808
+ ensureCloudWorkspaceReadyForSync
1122
1809
  });
1123
1810
  }, [
1811
+ saveWorkspaceCloudSyncSettingsBase,
1124
1812
  cloudAuthConfigured,
1125
1813
  isAuthenticated,
1126
- resolveCloudAuthUrl,
1127
- ensureCloudWorkspaceReadyForSync,
1128
- fetchLocalWorkspaceSnapshot,
1129
- saveWorkspaceCloudSyncSettingsBase
1814
+ ensureCloudWorkspaceReadyForSync
1130
1815
  ]);
1131
- // -----------------------------------------------------------------------
1132
- // Retry / reset functions
1133
- // -----------------------------------------------------------------------
1134
1816
  const retryUserGlobalSettingsSync = useCallback(async () => {
1135
1817
  await syncUserGlobalSettings({ preferCloudOnFirstSync: false });
1136
1818
  }, [syncUserGlobalSettings]);
1137
1819
  const retryWorkspaceCloudSync = useCallback(async () => {
1138
1820
  clearWorkspaceRetry();
1139
1821
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1140
- return;
1822
+ return false;
1141
1823
  if (!isAuthenticated) {
1142
1824
  const authed = await checkAuthSession();
1143
1825
  if (!authed)
1144
- return;
1826
+ return false;
1145
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;
1146
1838
  const signature = buildWorkspaceSyncSignature();
1147
- if (signature && signature !== workspaceLastPushedSignatureRef.current) {
1148
- void pushWorkspaceChangesToCloud(signature);
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;
1149
1849
  }
1150
- await pullWorkspaceChangesFromCloud();
1850
+ return pulled;
1151
1851
  }, [
1152
1852
  clearWorkspaceRetry,
1153
1853
  cloudAuthConfigured,
1154
1854
  runtimeMode,
1855
+ workspaceCloudSyncEnabled,
1155
1856
  isAuthenticated,
1156
1857
  checkAuthSession,
1157
- workspaceCloudSyncEnabled,
1858
+ workspaceSyncPhase,
1158
1859
  buildWorkspaceSyncSignature,
1860
+ shouldSuppressAttachBootstrapPush,
1159
1861
  pushWorkspaceChangesToCloud,
1160
1862
  pullWorkspaceChangesFromCloud
1161
1863
  ]);
1162
1864
  const resetWorkspaceSyncCursorAndPull = useCallback(async () => {
1163
- clearWorkspaceRetry();
1164
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1165
- return;
1166
- if (!isAuthenticated) {
1167
- const authed = await checkAuthSession();
1168
- if (!authed)
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');
1169
1871
  return;
1872
+ }
1170
1873
  }
1874
+ clearWorkspaceRetry();
1875
+ clearWorkspaceIssue();
1876
+ bootstrapPhaseStartedAtRef.current = null;
1877
+ bootstrapLastProgressAtRef.current = null;
1171
1878
  workspacePullCursorRef.current = null;
1172
- workspacePullBootstrapDoneRef.current = false;
1173
- const workspaceKey = String(currentWorkspaceId || '').trim();
1174
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
1175
- return;
1176
- workspaceCloudHydratedRef.current.delete(workspaceKey);
1177
- setWorkspaceCloudHydrated(false);
1178
- // Reset watermarks so the next push re-sends the full snapshot.
1879
+ workspaceAttachBootstrapBaselineSignatureRef.current = '';
1880
+ workspaceCaptureAttachBootstrapBaselineRef.current = false;
1881
+ workspacePendingSignatureRef.current = '';
1882
+ workspaceLastPushedSignatureRef.current = '';
1179
1883
  workspaceLastPushedWatermarksRef.current = new Map();
1180
- logSyncDebug('pull_cursor_reset', { workspaceId: currentWorkspaceId });
1181
- void persistWorkspaceSyncPatch({ pullCursor: null });
1182
- await pullWorkspaceChangesFromCloud();
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
+ }
1183
1941
  }, [
1942
+ acquireWorkspaceSyncLease,
1184
1943
  clearWorkspaceRetry,
1185
- cloudAuthConfigured,
1186
- runtimeMode,
1187
- isAuthenticated,
1188
- checkAuthSession,
1944
+ clearWorkspaceIssue,
1945
+ currentWorkspaceId,
1946
+ forceClearWorkspaceSyncLease,
1189
1947
  workspaceCloudSyncEnabled,
1948
+ workspaceSyncPhase,
1949
+ persistWorkspaceSyncPatch,
1950
+ buildWorkspaceSyncSignature,
1951
+ workspaceLastPullAt,
1952
+ pullWorkspaceChangesFromCloud,
1953
+ pushWorkspaceChangesToCloud,
1954
+ releaseWorkspaceSyncLease,
1955
+ reportWorkspaceWindowContention
1956
+ ]);
1957
+ // === SECTION: Diagnostics & reactive effects ===
1958
+ const getWorkspaceSyncDiagnostics = useCallback(() => {
1959
+ return {
1960
+ workspaceId: currentWorkspaceId,
1961
+ enabled: workspaceCloudSyncEnabled,
1962
+ phase: workspaceSyncPhase,
1963
+ status: workspaceSyncStatus,
1964
+ summary: workspaceSyncSummary,
1965
+ lastSuccessfulSyncAt: workspaceLastSuccessfulSyncAt,
1966
+ lastPullAt: workspaceLastPullAt,
1967
+ lastPushAt: workspaceLastPushAt,
1968
+ lastErrorAt: workspaceLastErrorAt,
1969
+ pendingChanges: workspaceSyncPendingChanges,
1970
+ lastErrorMessage: workspaceLastErrorMessage,
1971
+ recommendedAction: workspaceSyncRecommendedAction,
1972
+ documentSnapshotCount: workspaceSyncMarkdownDocuments.length,
1973
+ aiProfileSnapshotCount: workspaceSyncAiProfiles.length,
1974
+ aiProfileSnapshotRawCount: workspaceSyncAiProfilesRawCount,
1975
+ aiProfileSnapshotLastFetchAt: workspaceSyncAiProfilesLastFetchAt,
1976
+ aiProfileSnapshotLastFetchError: workspaceSyncAiProfilesLastFetchError,
1977
+ aiProfileSnapshotLastSkipReason: workspaceSyncAiProfilesLastSkipReason,
1978
+ assetSnapshotCount: workspaceSyncAssets.length,
1979
+ documentReviewSessionSnapshotCount: workspaceSyncDocumentReviewSessions.length,
1980
+ documentReviewSessionSnapshotFingerprint: workspaceSyncDocumentReviewSessionsFingerprint,
1981
+ annotatedAttachmentSessionSnapshotCount: workspaceSyncAnnotatedAttachmentSessions.length,
1982
+ annotatedAttachmentSessionSnapshotFingerprint: workspaceSyncAnnotatedAttachmentSessionsFingerprint,
1983
+ lastPushedAiProfileCount: workspaceLastPushedAiProfileIdsRef.current.size,
1984
+ lastPushedAiProfileWatermarkCount: workspaceLastPushedAiProfileWatermarksRef.current.size,
1985
+ forceFullAiProfilePushQueued: workspaceForceFullAiProfilePushRef.current === true
1986
+ };
1987
+ }, [
1190
1988
  currentWorkspaceId,
1191
- pullWorkspaceChangesFromCloud
1989
+ workspaceCloudSyncEnabled,
1990
+ workspaceSyncPhase,
1991
+ workspaceSyncStatus,
1992
+ workspaceSyncSummary,
1993
+ workspaceLastSuccessfulSyncAt,
1994
+ workspaceLastPullAt,
1995
+ workspaceLastPushAt,
1996
+ workspaceLastErrorAt,
1997
+ workspaceSyncPendingChanges,
1998
+ workspaceLastErrorMessage,
1999
+ workspaceSyncRecommendedAction,
2000
+ workspaceSyncMarkdownDocuments.length,
2001
+ workspaceSyncAiProfiles.length,
2002
+ workspaceSyncAiProfilesRawCount,
2003
+ workspaceSyncAiProfilesLastFetchAt,
2004
+ workspaceSyncAiProfilesLastFetchError,
2005
+ workspaceSyncAiProfilesLastSkipReason,
2006
+ workspaceSyncAssets.length,
2007
+ workspaceSyncDocumentReviewSessions.length,
2008
+ workspaceSyncDocumentReviewSessionsFingerprint,
2009
+ workspaceSyncAnnotatedAttachmentSessions.length,
2010
+ workspaceSyncAnnotatedAttachmentSessionsFingerprint
1192
2011
  ]);
1193
- // -----------------------------------------------------------------------
1194
- // Sync effects
1195
- // -----------------------------------------------------------------------
1196
- // Documents refresh interval
2012
+ useEffect(() => {
2013
+ retryWorkspaceSyncRef.current = retryWorkspaceCloudSync;
2014
+ }, [retryWorkspaceCloudSync]);
1197
2015
  useEffect(() => {
1198
2016
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1199
2017
  return;
1200
- void refreshWorkspaceSyncDocumentsSnapshot();
2018
+ if (!authSessionResolved || !isAuthenticated)
2019
+ return;
2020
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2021
+ void refreshWorkspaceSyncAiProfilesSnapshot();
2022
+ void refreshWorkspaceSyncAssetsSnapshot();
2023
+ void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2024
+ void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
2025
+ const handleContextAssetsMutated = (event) => {
2026
+ const detail = event.detail;
2027
+ const eventWorkspaceId = String(detail?.workspaceId || '').trim() || 'default';
2028
+ if (eventWorkspaceId !== currentWorkspaceId)
2029
+ return;
2030
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2031
+ void refreshWorkspaceSyncAiProfilesSnapshot();
2032
+ void refreshWorkspaceSyncAssetsSnapshot();
2033
+ void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2034
+ void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
2035
+ };
2036
+ window.addEventListener(TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT, handleContextAssetsMutated);
1201
2037
  const docsIntervalId = window.setInterval(() => {
1202
- void refreshWorkspaceSyncDocumentsSnapshot();
1203
- }, 12000);
1204
- return () => window.clearInterval(docsIntervalId);
2038
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2039
+ }, workspaceDocumentsRefreshIntervalMs);
2040
+ const aiProfilesIntervalId = window.setInterval(() => {
2041
+ void refreshWorkspaceSyncAiProfilesSnapshot();
2042
+ }, workspaceDocumentsRefreshIntervalMs);
2043
+ const assetsIntervalId = window.setInterval(() => {
2044
+ void refreshWorkspaceSyncAssetsSnapshot();
2045
+ }, workspaceDocumentsRefreshIntervalMs);
2046
+ const documentReviewSessionsIntervalId = window.setInterval(() => {
2047
+ void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2048
+ }, workspaceDocumentsRefreshIntervalMs);
2049
+ const annotatedAttachmentSessionsIntervalId = window.setInterval(() => {
2050
+ void refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot();
2051
+ }, workspaceDocumentsRefreshIntervalMs);
2052
+ return () => {
2053
+ window.removeEventListener(TASKFORCE_CONTEXT_ASSETS_MUTATED_EVENT, handleContextAssetsMutated);
2054
+ window.clearInterval(docsIntervalId);
2055
+ window.clearInterval(aiProfilesIntervalId);
2056
+ window.clearInterval(assetsIntervalId);
2057
+ window.clearInterval(documentReviewSessionsIntervalId);
2058
+ window.clearInterval(annotatedAttachmentSessionsIntervalId);
2059
+ };
1205
2060
  }, [
1206
2061
  cloudAuthConfigured,
1207
2062
  runtimeMode,
1208
2063
  workspaceCloudSyncEnabled,
1209
- refreshWorkspaceSyncDocumentsSnapshot
2064
+ authSessionResolved,
2065
+ isAuthenticated,
2066
+ currentWorkspaceId,
2067
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
2068
+ refreshWorkspaceSyncAiProfilesSnapshot,
2069
+ refreshWorkspaceSyncAssetsSnapshot,
2070
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
2071
+ refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
2072
+ workspaceDocumentsRefreshIntervalMs
1210
2073
  ]);
1211
- // Push on signature change
2074
+ // When task attachments change (e.g., after the MCP save_task_attachment tool writes a
2075
+ // file and the React polling detects the updated task), refresh the asset snapshot
2076
+ // immediately so the new binary is included in the next push payload. This closes the
2077
+ // gap where the MCP path cannot dispatch the browser-only context-assets-mutated event.
1212
2078
  useEffect(() => {
1213
2079
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1214
2080
  return;
2081
+ if (!authSessionResolved || !isAuthenticated)
2082
+ return;
2083
+ if (!taskAttachmentsFingerprint)
2084
+ 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]);
2091
+ useEffect(() => {
2092
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
2093
+ return;
2094
+ if (workspaceSyncPhase === 'attach-cloud')
2095
+ return;
2096
+ if (!workspaceSyncReferenceSnapshotsReady)
2097
+ return;
1215
2098
  const signature = buildWorkspaceSyncSignature();
2099
+ if (shouldSuppressAttachBootstrapPush(signature))
2100
+ return;
1216
2101
  if (!signature || signature === workspaceLastPushedSignatureRef.current)
1217
2102
  return;
1218
- if (workspacePushInFlightRef.current) {
2103
+ const pendingPayload = buildWorkspaceSyncPayload();
2104
+ setWorkspaceSyncPendingChanges(pendingPayload.changes.length);
2105
+ if (workspacePushInFlightRef.current || workspacePullInFlightRef.current) {
1219
2106
  workspacePendingSignatureRef.current = signature;
1220
2107
  return;
1221
2108
  }
@@ -1227,66 +2114,54 @@ export function useSyncOrchestrator(input) {
1227
2114
  cloudAuthConfigured,
1228
2115
  runtimeMode,
1229
2116
  workspaceCloudSyncEnabled,
2117
+ authSessionResolved,
2118
+ workspaceSyncPhase,
2119
+ workspaceSyncReferenceSnapshotsReady,
1230
2120
  buildWorkspaceSyncSignature,
1231
- workspaceSyncDocumentsFingerprint,
1232
- pushWorkspaceChangesToCloud
2121
+ shouldSuppressAttachBootstrapPush,
2122
+ buildWorkspaceSyncPayload,
2123
+ workspaceSyncAiProfilesFingerprint,
2124
+ workspaceSyncMarkdownDocumentsFingerprint,
2125
+ workspaceSyncAssetsFingerprint,
2126
+ workspaceSyncDocumentReviewSessionsFingerprint,
2127
+ workspaceSyncAnnotatedAttachmentSessionsFingerprint,
2128
+ pushWorkspaceChangesToCloud,
2129
+ workspacePushInFlightRef,
2130
+ workspacePullInFlightRef
1233
2131
  ]);
1234
- // Clear sync state when disabled
1235
2132
  useEffect(() => {
1236
2133
  if (cloudAuthConfigured && runtimeMode === 'local' && workspaceCloudSyncEnabled)
1237
2134
  return;
1238
2135
  clearWorkspaceRetry();
1239
- setWorkspaceSyncReady(false);
1240
- setWorkspaceCloudHydrated(false);
2136
+ setWorkspaceSyncBusy(false);
1241
2137
  }, [
1242
2138
  cloudAuthConfigured,
1243
2139
  runtimeMode,
1244
2140
  workspaceCloudSyncEnabled,
1245
2141
  clearWorkspaceRetry
1246
2142
  ]);
1247
- // Pull interval
1248
2143
  useEffect(() => {
1249
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
2144
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
2145
+ return;
2146
+ if (workspaceSyncPhase === 'provision-local')
1250
2147
  return;
1251
- if (isWorkspacePullRetryPending())
2148
+ if (isWorkspaceRetryPending())
1252
2149
  return;
1253
2150
  void pullWorkspaceChangesFromCloud();
1254
2151
  const intervalId = window.setInterval(() => {
1255
2152
  void pullWorkspaceChangesFromCloud();
1256
- }, 8000);
2153
+ }, workspacePullIntervalMs);
1257
2154
  return () => window.clearInterval(intervalId);
1258
2155
  }, [
1259
2156
  cloudAuthConfigured,
1260
2157
  runtimeMode,
1261
2158
  workspaceCloudSyncEnabled,
1262
- isWorkspacePullRetryPending,
1263
- pullWorkspaceChangesFromCloud
1264
- ]);
1265
- // Backfill: pull when local is empty
1266
- useEffect(() => {
1267
- if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1268
- return;
1269
- if (tasks.length > 0 || archivedTasks.length > 0)
1270
- return;
1271
- const workspaceKey = String(currentWorkspaceId || '').trim();
1272
- if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
1273
- return;
1274
- if (workspaceBackfillForcedRef.current.has(workspaceKey))
1275
- return;
1276
- workspaceBackfillForcedRef.current.add(workspaceKey);
1277
- workspacePullCursorRef.current = null;
1278
- workspacePullBootstrapDoneRef.current = false;
1279
- void pullWorkspaceChangesFromCloud();
1280
- }, [
1281
- cloudAuthConfigured,
1282
- runtimeMode,
1283
- workspaceCloudSyncEnabled,
1284
- currentWorkspaceId,
1285
- tasks.length,
1286
- archivedTasks.length,
1287
- pullWorkspaceChangesFromCloud
2159
+ authSessionResolved,
2160
+ workspaceSyncPhase,
2161
+ isWorkspaceRetryPending,
2162
+ pullWorkspaceChangesFromCloud,
2163
+ workspacePullIntervalMs
1288
2164
  ]);
1289
- // User global settings: initial sync on auth
1290
2165
  useEffect(() => {
1291
2166
  if (runtimeMode !== 'local' || !isAuthenticated)
1292
2167
  return;
@@ -1294,7 +2169,6 @@ export function useSyncOrchestrator(input) {
1294
2169
  return;
1295
2170
  void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1296
2171
  }, [runtimeMode, isAuthenticated, authUserId, syncUserGlobalSettings]);
1297
- // User global settings: push on settings change
1298
2172
  useEffect(() => {
1299
2173
  if (runtimeMode !== 'local' || !isAuthenticated)
1300
2174
  return;
@@ -1313,7 +2187,6 @@ export function useSyncOrchestrator(input) {
1313
2187
  if (signature === lastUserGlobalSyncSignatureRef.current)
1314
2188
  return;
1315
2189
  lastUserGlobalSyncSignatureRef.current = signature;
1316
- setUserGlobalSyncPendingChanges(1);
1317
2190
  setLocalUserSyncUpdatedAt(authUserId, new Date().toISOString());
1318
2191
  const timeoutId = window.setTimeout(() => {
1319
2192
  void syncUserGlobalSettings({ preferCloudOnFirstSync: false });
@@ -1327,61 +2200,77 @@ export function useSyncOrchestrator(input) {
1327
2200
  setLocalUserSyncUpdatedAt,
1328
2201
  syncUserGlobalSettings
1329
2202
  ]);
1330
- // -----------------------------------------------------------------------
1331
- // Return
1332
- // -----------------------------------------------------------------------
2203
+ // On first workspace activation, call the server-side startup repair endpoint once.
2204
+ // This re-registers DB records for asset/document files that exist on disk but have
2205
+ // missing or stale-tombstoned metadata — a common result of a crash between file write
2206
+ // and DB upsert. The repair only adds missing records; it never deletes files.
2207
+ useEffect(() => {
2208
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
2209
+ return;
2210
+ if (!authSessionResolved || !isAuthenticated)
2211
+ return;
2212
+ if (workspaceSyncPhase === 'provision-local' || workspaceSyncPhase === 'attach-cloud')
2213
+ return;
2214
+ if (workspaceStartupRepairRanRef.current)
2215
+ return;
2216
+ workspaceStartupRepairRanRef.current = true;
2217
+ fetch('/api/taskforce/sync/workspace/repair-startup', {
2218
+ method: 'POST',
2219
+ credentials: 'include',
2220
+ headers: { 'Content-Type': 'application/json' },
2221
+ body: JSON.stringify({ workspaceId: currentWorkspaceId })
2222
+ }).then(async (res) => {
2223
+ if (!res.ok)
2224
+ return;
2225
+ const data = await res.json().catch(() => ({}));
2226
+ if (data?.applied > 0) {
2227
+ logSyncDebug('startup_repair_applied', {
2228
+ workspaceId: currentWorkspaceId,
2229
+ applied: data.applied,
2230
+ skipped: data.skipped,
2231
+ failed: data.failed
2232
+ });
2233
+ // Refresh snapshots so newly recovered assets are included in the next push
2234
+ void refreshWorkspaceSyncMarkdownDocumentsSnapshot();
2235
+ void refreshWorkspaceSyncAiProfilesSnapshot();
2236
+ void refreshWorkspaceSyncAssetsSnapshot();
2237
+ void refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
2238
+ }
2239
+ }).catch(() => { });
2240
+ }, [
2241
+ cloudAuthConfigured,
2242
+ runtimeMode,
2243
+ workspaceCloudSyncEnabled,
2244
+ authSessionResolved,
2245
+ isAuthenticated,
2246
+ workspaceSyncPhase,
2247
+ currentWorkspaceId,
2248
+ refreshWorkspaceSyncMarkdownDocumentsSnapshot,
2249
+ refreshWorkspaceSyncAiProfilesSnapshot,
2250
+ refreshWorkspaceSyncAssetsSnapshot,
2251
+ refreshWorkspaceSyncDocumentReviewSessionsSnapshot
2252
+ ]);
2253
+ // === SECTION: Return value ===
1333
2254
  return {
1334
- // State
1335
2255
  userGlobalSyncStatus,
1336
2256
  setUserGlobalSyncStatus,
1337
- userGlobalLastSyncedAt,
1338
- setUserGlobalLastSyncedAt,
1339
- userGlobalSyncPendingChanges,
1340
- setUserGlobalSyncPendingChanges,
1341
2257
  userGlobalSyncError,
1342
2258
  setUserGlobalSyncError,
1343
2259
  workspaceLastPullAt,
1344
- setWorkspaceLastPullAt,
1345
2260
  workspaceLastPushAt,
1346
- setWorkspaceLastPushAt,
1347
2261
  workspaceLastErrorAt,
1348
- setWorkspaceLastErrorAt,
1349
2262
  workspaceLastErrorMessage,
1350
- setWorkspaceLastErrorMessage,
1351
- workspaceLastWarningMessage,
1352
- setWorkspaceLastWarningMessage,
2263
+ workspaceLastSuccessfulSyncAt,
1353
2264
  workspaceCloudSyncEnabled,
1354
- realtimeConnectionState,
1355
- realtimeTelemetry,
1356
- setWorkspaceCloudSyncEnabled,
1357
- workspaceSyncSourceOfTruth,
1358
- setWorkspaceSyncSourceOfTruth,
1359
- workspaceSyncOnboardingCompleted,
1360
- setWorkspaceSyncOnboardingCompleted,
1361
- workspaceSyncReady,
1362
- setWorkspaceSyncReady,
1363
- workspaceCloudHydrated,
1364
- setWorkspaceCloudHydrated,
1365
- workspaceSyncDocuments,
1366
- setWorkspaceSyncDocuments,
1367
- workspaceSyncDocumentsFingerprint,
1368
- setWorkspaceSyncDocumentsFingerprint,
1369
- // Refs
2265
+ workspaceSyncPhase,
2266
+ workspaceSyncStatus,
2267
+ workspaceSyncSummary,
2268
+ workspaceSyncRecommendedAction,
2269
+ workspaceSyncBusy,
2270
+ workspaceSyncPendingChanges,
1370
2271
  syncInFlightRef,
1371
- // From useWorkspaceSyncController
1372
2272
  workspaceRetryAt,
1373
- isWorkspacePullRetryPending,
1374
- workspaceBootstrapSyncInProgress,
1375
- setWorkspaceBootstrapSyncInProgress,
1376
- workspaceBootstrapSyncPages,
1377
- setWorkspaceBootstrapSyncPages,
1378
- workspaceBootstrapSyncAppliedChanges,
1379
- setWorkspaceBootstrapSyncAppliedChanges,
1380
- workspaceSyncFailureCount,
1381
- workspaceSyncRetryScheduledCount,
1382
- workspaceSyncAuthFailureCount,
1383
- workspaceSyncLastFailureSource,
1384
- workspaceSyncLastFailureStatusCode,
2273
+ isWorkspaceRetryPending,
1385
2274
  workspacePushInFlightRef,
1386
2275
  workspacePullInFlightRef,
1387
2276
  workspaceLastPushedSignatureRef,
@@ -1389,39 +2278,18 @@ export function useSyncOrchestrator(input) {
1389
2278
  workspaceLastPushedTaskIdsRef,
1390
2279
  workspaceDeletedTaskIdsRef,
1391
2280
  workspacePullCursorRef,
1392
- workspacePullBootstrapDoneRef,
1393
- workspaceBackfillForcedRef,
1394
- workspaceSyncReadyRef,
1395
- workspaceCloudHydratedRef,
1396
2281
  clearWorkspaceRetry,
1397
- scheduleWorkspacePullRetry,
1398
- isWorkspacePullCooldownActive,
1399
- markWorkspacePullCooldown,
1400
- tryAcquireWorkspacePullLease,
1401
- releaseWorkspacePullLease,
1402
- recordWorkspaceSyncFailure,
1403
- incrementWorkspaceSyncAuthFailure,
1404
2282
  persistWorkspaceSyncPatch,
1405
2283
  loadWorkspaceSyncState,
1406
2284
  applyWorkspaceSyncStateSnapshot,
1407
- // Functions
1408
2285
  syncUserGlobalSettings,
1409
- getLocalUserSyncUpdatedAt,
1410
- setLocalUserSyncUpdatedAt,
1411
- buildUserGlobalSyncPayload,
1412
- applyRemoteUserGlobalSyncPayload,
1413
- ensureCloudWorkspaceReadyForSync,
1414
- buildWorkspaceSyncPayload,
1415
2286
  buildWorkspaceSyncSignature,
1416
- refreshWorkspaceSyncDocumentsSnapshot,
1417
- handleSyncAuthFailure,
1418
- ensureWorkspaceSyncReady,
1419
2287
  pushWorkspaceChangesToCloud,
1420
2288
  pullWorkspaceChangesFromCloud,
1421
- fetchLocalWorkspaceSnapshot,
1422
2289
  saveWorkspaceCloudSyncSettings,
1423
2290
  retryUserGlobalSettingsSync,
1424
2291
  retryWorkspaceCloudSync,
1425
- resetWorkspaceSyncCursorAndPull
2292
+ resetWorkspaceSyncCursorAndPull,
2293
+ getWorkspaceSyncDiagnostics
1426
2294
  };
1427
2295
  }