@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.
- package/README.md +71 -21
- package/dist/Taskforce.module.css +3394 -5981
- package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
- package/dist/TaskforceCore.filter-persistence.test.js +402 -0
- package/dist/TaskforceCore.js +388 -71
- package/dist/TaskforceCore.routing.test.js +311 -0
- package/dist/TaskforceCore.test.js +3344 -615
- package/dist/TaskforceWidget.js +3 -2
- package/dist/__tests__/localizationCatalogs.test.js +48 -31
- package/dist/__tests__/workflowSimulation.test.js +2 -2
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
- package/dist/annotatedAttachments/payload.d.ts +2 -0
- package/dist/annotatedAttachments/payload.js +54 -0
- package/dist/annotatedAttachments/payload.test.js +67 -0
- package/dist/annotatedAttachments/service.d.ts +49 -0
- package/dist/annotatedAttachments/service.js +121 -0
- package/dist/annotatedAttachments/types.d.ts +86 -0
- package/dist/cli.js +74 -25
- package/dist/compat/deploymentCompat.d.ts +12 -0
- package/dist/compat/deploymentCompat.js +43 -0
- package/dist/compat/workspaceSyncCompat.d.ts +6 -0
- package/dist/compat/workspaceSyncCompat.js +79 -0
- package/dist/compat/workspaceSyncCompat.test.js +87 -0
- package/dist/components/features/AgentsModule.d.ts +5 -0
- package/dist/components/features/AgentsModule.js +131 -0
- package/dist/components/features/AgentsModule.test.js +102 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
- package/dist/components/features/DocumentGenerateModal.js +8 -16
- package/dist/components/features/DocumentIndex.d.ts +3 -4
- package/dist/components/features/DocumentIndex.js +22 -10
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +1000 -18
- package/dist/components/features/DocumentWorkspace.d.ts +40 -2
- package/dist/components/features/DocumentWorkspace.js +96 -16
- package/dist/components/features/DocumentWorkspace.test.js +1202 -0
- package/dist/components/features/InitiativesModule.d.ts +22 -0
- package/dist/components/features/InitiativesModule.js +33 -0
- package/dist/components/features/TaskSettings.d.ts +41 -32
- package/dist/components/features/TaskSettings.js +798 -313
- package/dist/components/features/TaskSettings.test.js +1221 -47
- package/dist/components/features/WorkflowsModule.d.ts +27 -0
- package/dist/components/features/WorkflowsModule.js +21 -0
- package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
- package/dist/components/features/documentWorkspaceFilters.js +11 -0
- package/dist/components/features/settings/CategoryManager.d.ts +2 -0
- package/dist/components/features/settings/CategoryManager.js +15 -8
- package/dist/components/features/settings/PriorityManager.d.ts +18 -0
- package/dist/components/features/settings/PriorityManager.js +51 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
- package/dist/components/features/settings/TaxonomyEditor.js +41 -18
- package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
- package/dist/components/features/settings/TaxonomyManager.js +59 -27
- package/dist/components/features/settings/TypeManager.d.ts +11 -0
- package/dist/components/features/settings/TypeManager.js +48 -11
- package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
- package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
- package/dist/components/task/TaskActionHeader.d.ts +12 -2
- package/dist/components/task/TaskActionHeader.js +64 -8
- package/dist/components/task/TaskActionHeader.test.js +73 -0
- package/dist/components/task/TaskCard.d.ts +14 -16
- package/dist/components/task/TaskCard.js +144 -174
- package/dist/components/task/TaskCard.test.js +416 -64
- package/dist/components/task/TaskContextUpload.d.ts +6 -5
- package/dist/components/task/TaskContextUpload.js +199 -64
- package/dist/components/task/TaskContextUpload.test.js +331 -14
- package/dist/components/task/TaskForm.d.ts +23 -29
- package/dist/components/task/TaskForm.js +348 -169
- package/dist/components/task/TaskForm.test.js +419 -23
- package/dist/components/task/TaskKanban.d.ts +20 -21
- package/dist/components/task/TaskKanban.js +176 -316
- package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
- package/dist/components/task/TaskList.d.ts +22 -11
- package/dist/components/task/TaskList.js +98 -12
- package/dist/components/task/TaskList.test.js +206 -7
- package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
- package/dist/components/task/TaskReferenceBadge.js +5 -0
- package/dist/components/task/TaskStatusActions.d.ts +12 -0
- package/dist/components/task/TaskStatusActions.js +18 -0
- package/dist/components/task/TaskStatusActions.test.js +45 -0
- package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
- package/dist/components/ui/ImageReferenceBadge.js +5 -0
- package/dist/components/ui/LevelSelector.d.ts +2 -1
- package/dist/components/ui/LevelSelector.js +2 -2
- package/dist/components/ui/Markdown.d.ts +1 -2
- package/dist/components/ui/Markdown.js +2 -13
- package/dist/components/ui/Modal.d.ts +3 -2
- package/dist/components/ui/Modal.js +4 -3
- package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
- package/dist/components/ui/MultiSelectFilter.js +2 -2
- package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
- package/dist/components/ui/ReferenceBadgeButton.js +6 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
- package/dist/components/ui/TaxonomyDropdown.js +58 -23
- package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
- package/dist/components/ui/TopNoticeLayer.js +14 -0
- package/dist/components/ui/TopNoticeLayer.test.js +25 -0
- package/dist/components/views/AppShellHeader.d.ts +13 -0
- package/dist/components/views/AppShellHeader.js +19 -0
- package/dist/components/views/AppShellHeader.test.js +21 -0
- package/dist/components/views/PlanComparisonPage.d.ts +73 -0
- package/dist/components/views/PlanComparisonPage.js +196 -0
- package/dist/components/views/PlanComparisonPage.test.js +251 -0
- package/dist/components/views/PlansPage.d.ts +14 -0
- package/dist/components/views/PlansPage.js +355 -0
- package/dist/components/views/StandaloneLayout.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +2227 -1188
- package/dist/components/views/WidgetView.d.ts +2 -2
- package/dist/components/views/WidgetView.js +149 -43
- package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
- package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
- package/dist/components/views/chrome/workspaceChrome.js +108 -0
- package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
- package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
- package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
- package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
- package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
- package/dist/components/views/panels/FilterToolbar.js +21 -0
- package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbar.test.js +45 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
- package/dist/components/views/panels/FilterToolbarControls.js +61 -0
- package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
- package/dist/components/views/panels/PlanningDrawer.js +220 -0
- package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
- package/dist/components/views/panels/ScheduleSidebar.js +127 -0
- package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
- package/dist/components/views/panels/scheduleUtils.js +79 -0
- package/dist/components/views/teamManagementAccess.d.ts +6 -0
- package/dist/components/views/teamManagementAccess.js +18 -0
- package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
- package/dist/components/views/teamManagementAccess.test.js +44 -0
- package/dist/config/clientDeployment.d.ts +2 -0
- package/dist/config/clientDeployment.js +13 -0
- package/dist/config/cloudEnvironment.d.ts +15 -0
- package/dist/config/cloudEnvironment.js +60 -0
- package/dist/config/deployment.d.ts +20 -0
- package/dist/config/deployment.js +67 -0
- package/dist/config/deployment.test.d.ts +1 -0
- package/dist/config/deployment.test.js +88 -0
- package/dist/config/envSchema.js +36 -8
- package/dist/config/envSchema.test.js +28 -0
- package/dist/core/AiProfileService.d.ts +66 -0
- package/dist/core/AiProfileService.js +320 -0
- package/dist/core/AiProfileService.test.d.ts +1 -0
- package/dist/core/AiProfileService.test.js +164 -0
- package/dist/core/AiProfiles.test.d.ts +1 -0
- package/dist/core/AiProfiles.test.js +449 -0
- package/dist/core/AttachmentLinkService.d.ts +15 -0
- package/dist/core/AttachmentLinkService.js +157 -0
- package/dist/core/AuthTokenService.d.ts +44 -0
- package/dist/core/AuthTokenService.js +207 -0
- package/dist/core/CreatedBy.test.js +134 -6
- package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
- package/dist/core/DeletedTaskLifecycleService.js +158 -0
- package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
- package/dist/core/DeletedTaskTrash.test.js +104 -0
- package/dist/core/EntitlementsPolicy.test.js +78 -5
- package/dist/core/GlobalSettingsService.d.ts +16 -0
- package/dist/core/GlobalSettingsService.js +370 -0
- package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
- package/dist/core/JsonBackupRuntime.test.js +155 -0
- package/dist/core/McpTokenService.d.ts +118 -0
- package/dist/core/McpTokenService.js +921 -0
- package/dist/core/PlanEntitlementService.d.ts +201 -0
- package/dist/core/PlanEntitlementService.js +1442 -0
- package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
- package/dist/core/PlanVersionPolicy.test.js +178 -0
- package/dist/core/PlanningEntities.test.d.ts +1 -0
- package/dist/core/PlanningEntities.test.js +95 -0
- package/dist/core/ScheduleFields.test.js +15 -19
- package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
- package/dist/core/SignupMonetizationSettings.test.js +159 -0
- package/dist/core/Stats.test.d.ts +1 -0
- package/dist/core/Stats.test.js +36 -0
- package/dist/core/SyncReconciliationService.d.ts +93 -0
- package/dist/core/SyncReconciliationService.js +406 -0
- package/dist/core/SystemAdmin.test.js +578 -13
- package/dist/core/TaskActivityService.d.ts +21 -0
- package/dist/core/TaskActivityService.js +130 -0
- package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
- package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
- package/dist/core/TaskAuditTimeline.test.js +132 -0
- package/dist/core/TaskTaxonomyValidation.test.js +338 -15
- package/dist/core/Taskforce.d.ts +930 -183
- package/dist/core/Taskforce.ids.test.js +252 -0
- package/dist/core/Taskforce.js +3848 -2532
- package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
- package/dist/core/Taskforce.mcpAuth.test.js +343 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
- package/dist/core/TaxonomySettingsService.d.ts +18 -0
- package/dist/core/TaxonomySettingsService.js +162 -0
- package/dist/core/WorkspaceDelete.test.js +32 -13
- package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
- package/dist/core/WorkspaceLifecycleService.js +88 -0
- package/dist/core/WorkspacePermissions.test.js +95 -0
- package/dist/core/WorkspacePlanMode.test.js +114 -1
- package/dist/core/WorkspacePreferenceService.d.ts +41 -0
- package/dist/core/WorkspacePreferenceService.js +246 -0
- package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
- package/dist/core/WorkspaceSyncSettings.test.js +65 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
- package/dist/core/shared.d.ts +36 -0
- package/dist/core/shared.js +128 -0
- package/dist/core/types.d.ts +614 -0
- package/dist/core/types.js +27 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
- package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
- package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
- package/dist/documentReviews/service.d.ts +68 -0
- package/dist/documentReviews/service.js +210 -0
- package/dist/documentReviews/types.d.ts +58 -0
- package/dist/documentReviews/types.js +1 -0
- package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
- package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
- package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskActions.js +2 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
- package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFilters.js +2 -0
- package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
- package/dist/hooks/tasks/useTaskFormActions.js +365 -0
- package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
- package/dist/hooks/tasks/useTaskRelationships.js +64 -0
- package/dist/hooks/ui/useResourceExport.d.ts +19 -0
- package/dist/hooks/ui/useResourceExport.js +84 -0
- package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
- package/dist/hooks/ui/useSettingsModel.js +125 -0
- package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
- package/dist/hooks/ui/useSettingsPersistence.js +254 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
- package/dist/hooks/ui/useUiNotice.d.ts +1 -0
- package/dist/hooks/ui/useUiNotice.js +1 -0
- package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
- package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
- package/dist/hooks/ui/useZenMode.d.ts +5 -0
- package/dist/hooks/ui/useZenMode.js +32 -0
- package/dist/hooks/useFilterSort.d.ts +60 -0
- package/dist/hooks/useFilterSort.js +362 -0
- package/dist/hooks/useFilterSort.test.d.ts +1 -0
- package/dist/hooks/useFilterSort.test.js +333 -0
- package/dist/hooks/useRealtimeSync.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.js +24 -9
- package/dist/hooks/useRealtimeSync.test.js +28 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
- package/dist/hooks/useSyncOrchestrator.js +1538 -670
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
- package/dist/hooks/useTaskData.d.ts +35 -0
- package/dist/hooks/useTaskData.js +243 -0
- package/dist/hooks/useTaskData.test.d.ts +1 -0
- package/dist/hooks/useTaskData.test.js +180 -0
- package/dist/hooks/useTaskMutations.d.ts +46 -0
- package/dist/hooks/useTaskMutations.js +472 -0
- package/dist/hooks/useTaskMutations.test.d.ts +1 -0
- package/dist/hooks/useTaskMutations.test.js +80 -0
- package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.contract.test.js +7 -0
- package/dist/hooks/useTaskforce.d.ts +260 -295
- package/dist/hooks/useTaskforce.js +1954 -2623
- package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
- package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
- package/dist/hooks/useUiNotice.d.ts +16 -0
- package/dist/hooks/useUiNotice.js +49 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
- package/dist/hooks/useWorkspaceSyncController.js +280 -461
- package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
- package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
- package/dist/hooks/workspace/workspaceLocalState.js +101 -0
- package/dist/localization/locales/en-US.d.ts +1 -8
- package/dist/localization/locales/en-US.js +4 -11
- package/dist/localization/locales/es-419.js +2 -9
- package/dist/localization/locales/pt-BR.js +2 -9
- package/dist/localization/localizationCatalogs.test.d.ts +1 -0
- package/dist/localization/localizationCatalogs.test.js +37 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -15
- package/dist/mcp/clientIntegrations.d.ts +33 -0
- package/dist/mcp/clientIntegrations.js +199 -0
- package/dist/mcp/clientIntegrations.test.d.ts +1 -0
- package/dist/mcp/clientIntegrations.test.js +110 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/optionResolution.test.js +6 -2
- package/dist/mcp/runtime.d.ts +120 -0
- package/dist/mcp/runtime.js +5668 -0
- package/dist/mcp/runtime.test.d.ts +1 -0
- package/dist/mcp/runtime.test.js +3299 -0
- package/dist/mcp/server.js +2 -1793
- package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
- package/dist/mcp/structuredDocValidation.js +1 -2
- package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
- package/dist/mcp/toolRegistry.d.ts +570 -0
- package/dist/mcp/toolRegistry.js +90 -0
- package/dist/mcp/toolRegistry.test.d.ts +1 -0
- package/dist/mcp/toolRegistry.test.js +30 -0
- package/dist/migrations/billingSchemaParity.test.js +111 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
- package/dist/migrations/taskSchemaMigrations.js +681 -39
- package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.test.js +210 -0
- package/dist/plugins/vite.js +13 -7
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/favicon.ico +0 -0
- package/dist/public/archive/phoenix/site.webmanifest +1 -0
- package/dist/public/favicon-96x96.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.svg +17 -0
- package/dist/public/site.webmanifest +21 -1
- package/dist/public/web-app-manifest-192x192.png +0 -0
- package/dist/public/web-app-manifest-512x512.png +0 -0
- package/dist/releaseVersion.test.d.ts +1 -0
- package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/dist/resources/templates/workflows/collaborate.yaml +11 -5
- package/dist/resources/templates/workflows/do.yaml +8 -2
- package/dist/resources/templates/workflows/evaluate.yaml +13 -7
- package/dist/resources/templates/workflows/execute.yaml +40 -23
- package/dist/resources/templates/workflows/plan.yaml +15 -8
- package/dist/resources/templates/workflows/review.yaml +14 -8
- package/dist/runtime/appGateDefinitions.d.ts +32 -0
- package/dist/runtime/appGateDefinitions.js +32 -0
- package/dist/runtime/appGates.d.ts +29 -0
- package/dist/runtime/appGates.js +53 -0
- package/dist/runtime/appGates.test.d.ts +1 -0
- package/dist/runtime/appGates.test.js +42 -0
- package/dist/runtime/migrationFlags.d.ts +1 -0
- package/dist/runtime/migrationFlags.js +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
- package/dist/server/auth.js +14 -2
- package/dist/server/auth.test.js +21 -1
- package/dist/server/cors.js +11 -5
- package/dist/server/cors.test.js +6 -0
- package/dist/server/documentReviewRoutes.test.d.ts +1 -0
- package/dist/server/documentReviewRoutes.test.js +157 -0
- package/dist/server/index.cookieProxy.test.js +46 -1
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +168 -41
- package/dist/server/index.rateLimit.test.js +6 -0
- package/dist/server/index.test.js +133 -2
- package/dist/server/mockStripe.d.ts +72 -0
- package/dist/server/mockStripe.js +357 -0
- package/dist/server/realtimeSyncWs.js +4 -0
- package/dist/server/routes/admin.d.ts +114 -0
- package/dist/server/routes/admin.js +2226 -0
- package/dist/server/routes/annotatedAttachments.d.ts +24 -0
- package/dist/server/routes/annotatedAttachments.js +235 -0
- package/dist/server/routes/auth.d.ts +61 -0
- package/dist/server/routes/auth.js +1296 -0
- package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
- package/dist/server/routes/billing.js +2001 -0
- package/dist/server/routes/billing.test.d.ts +1 -0
- package/dist/server/routes/billing.test.js +1631 -0
- package/dist/server/routes/documentReviews.d.ts +11 -0
- package/dist/server/routes/documentReviews.js +191 -0
- package/dist/server/routes/documents.d.ts +118 -0
- package/dist/server/routes/documents.js +1113 -0
- package/dist/server/routes/resources.d.ts +25 -0
- package/dist/server/routes/resources.js +225 -0
- package/dist/server/routes/shared.d.ts +35 -0
- package/dist/server/routes/shared.js +305 -0
- package/dist/server/routes/shared.test.d.ts +1 -0
- package/dist/server/routes/shared.test.js +32 -0
- package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
- package/dist/server/routes/sync.integration.test.d.ts +1 -0
- package/dist/server/routes/sync.integration.test.js +2003 -0
- package/dist/server/routes/sync.js +3143 -0
- package/dist/server/routes/tasks.d.ts +72 -0
- package/dist/server/routes/tasks.js +1252 -0
- package/dist/server/routes/workspaces.d.ts +32 -0
- package/dist/server/routes/workspaces.js +307 -0
- package/dist/server/routes.d.ts +8 -9
- package/dist/server/routes.js +968 -4092
- package/dist/server/routes.test.js +9043 -2415
- package/dist/server/runtimeMode.d.ts +1 -1
- package/dist/server/runtimeMode.js +3 -18
- package/dist/server/runtimeMode.test.js +3 -3
- package/dist/server/securityHeaders.d.ts +1 -1
- package/dist/server/securityHeaders.js +65 -6
- package/dist/server/securityHeaders.test.js +40 -2
- package/dist/services/templateEngine.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
- package/dist/services/templates.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templates.test.js +1 -1
- package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
- package/dist/services/workflowExportMatrix.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
- package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/services/workflowGeneration.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.js +113 -0
- package/dist/services/workflowV2Policy.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
- package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
- package/dist/shared/aiProfileSurfaceType.js +49 -0
- package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
- package/dist/shared/customTaxonomyLibrary.js +276 -0
- package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/customTaxonomyLibrary.test.js +20 -0
- package/dist/shared/taxonomyCatalog.d.ts +19 -0
- package/dist/shared/taxonomyCatalog.js +36 -0
- package/dist/shared/taxonomyDefaults.d.ts +29 -0
- package/dist/shared/taxonomyDefaults.js +26 -0
- package/dist/shared/taxonomyLibrary.d.ts +15 -0
- package/dist/shared/taxonomyLibrary.js +98 -0
- package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/taxonomyLibrary.test.js +26 -0
- package/dist/shared/taxonomyState.d.ts +36 -0
- package/dist/shared/taxonomyState.js +73 -0
- package/dist/shared/taxonomyState.test.d.ts +1 -0
- package/dist/shared/taxonomyState.test.js +71 -0
- package/dist/shared/workflowDefaults.d.ts +9 -0
- package/dist/shared/workflowDefaults.js +11 -0
- package/dist/storage/documentIntegrity.d.ts +2 -0
- package/dist/storage/documentIntegrity.js +34 -17
- package/dist/storage/documentPurge.d.ts +2 -1
- package/dist/storage/documentPurge.js +35 -8
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/objectStorage.d.ts +1 -1
- package/dist/storage/objectStorageClient.d.ts +6 -4
- package/dist/storage/objectStorageClient.js +77 -29
- package/dist/storage/objectStorageClient.test.d.ts +1 -0
- package/dist/storage/objectStorageClient.test.js +57 -0
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.d.ts +1 -0
- package/dist/storage/objectStorageConfig.test.js +38 -0
- package/dist/storage/postgresAdapter.d.ts +5 -0
- package/dist/storage/postgresAdapter.js +2 -2
- package/dist/storage/postgresAdapter.test.d.ts +1 -0
- package/dist/storage/postgresAdapter.test.js +29 -0
- package/dist/storage/taskAssetStore.d.ts +7 -4
- package/dist/storage/taskAssetStore.js +65 -60
- package/dist/storage/workspaceAssetStore.d.ts +122 -0
- package/dist/storage/workspaceAssetStore.js +496 -0
- package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
- package/dist/storage/workspaceAssetStore.test.js +501 -0
- package/dist/sync/cloudSyncApi.d.ts +33 -2
- package/dist/sync/cloudSyncApi.js +37 -15
- package/dist/sync/cloudSyncApi.test.js +60 -3
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/syncService.d.ts +25 -0
- package/dist/sync/syncService.js +184 -16
- package/dist/sync/syncService.test.js +289 -25
- package/dist/sync/workspaceRepair.d.ts +114 -0
- package/dist/sync/workspaceRepair.js +566 -0
- package/dist/sync/workspaceSyncModel.d.ts +230 -2
- package/dist/sync/workspaceSyncModel.js +456 -17
- package/dist/sync/workspaceSyncModel.test.js +642 -0
- package/dist/sync/workspaceSyncState.d.ts +12 -0
- package/dist/sync/workspaceSyncState.js +12 -0
- package/dist/sync/workspaceSyncSurface.d.ts +33 -0
- package/dist/sync/workspaceSyncSurface.js +74 -0
- package/dist/types.d.ts +143 -18
- package/dist/types.js +9 -24
- package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
- package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
- package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
- package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
- package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
- package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
- package/dist/ui/assets/index-xBWLPOEb.js +6 -0
- package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
- package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
- package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
- package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
- package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
- package/dist/ui/branding/logos/logo.png +0 -0
- package/dist/ui/branding/logos/logo_dark.svg +27 -0
- package/dist/ui/branding/logos/logo_favicon.svg +35 -0
- package/dist/ui/branding/logos/logo_light.svg +27 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
- package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
- package/dist/ui/favicon/favicon-96x96.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.svg +17 -0
- package/dist/ui/favicon/site.webmanifest +21 -1
- package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/ui/index.html +10 -7
- package/dist/ui/product/screen_01.png +0 -0
- package/dist/ui/product/screen_02.png +0 -0
- package/dist/ui/product/screen_03.png +0 -0
- package/dist/utils/annotatedAttachments.d.ts +20 -0
- package/dist/utils/annotatedAttachments.js +74 -0
- package/dist/utils/annotatedAttachments.test.d.ts +1 -0
- package/dist/utils/annotatedAttachments.test.js +108 -0
- package/dist/utils/assignees.d.ts +26 -0
- package/dist/utils/assignees.js +132 -0
- package/dist/utils/canonicalAssetKind.d.ts +16 -0
- package/dist/utils/canonicalAssetKind.js +39 -0
- package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
- package/dist/utils/canonicalAssetKind.test.js +24 -0
- package/dist/utils/coalescedRefresh.d.ts +5 -0
- package/dist/utils/coalescedRefresh.js +16 -0
- package/dist/utils/coalescedRefresh.test.d.ts +1 -0
- package/dist/utils/coalescedRefresh.test.js +30 -0
- package/dist/utils/constants.d.ts +16 -3
- package/dist/utils/constants.icons.test.js +2 -1
- package/dist/utils/constants.js +70 -13
- package/dist/utils/contextAssetEvents.d.ts +7 -0
- package/dist/utils/contextAssetEvents.js +6 -0
- package/dist/utils/contextFiles.d.ts +14 -3
- package/dist/utils/contextFiles.js +40 -8
- package/dist/utils/documentManagerApi.d.ts +6 -0
- package/dist/utils/documentManagerApi.js +19 -0
- package/dist/utils/documentNames.d.ts +24 -0
- package/dist/utils/documentNames.js +91 -0
- package/dist/utils/documentReferences.d.ts +9 -0
- package/dist/utils/documentReferences.js +11 -0
- package/dist/utils/entityReferences.d.ts +9 -0
- package/dist/utils/entityReferences.js +32 -0
- package/dist/utils/imageReferences.d.ts +9 -0
- package/dist/utils/imageReferences.js +11 -0
- package/dist/utils/planParser.d.ts +2 -2
- package/dist/utils/planParser.js +11 -11
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/{__tests__ → utils}/planParser.test.js +22 -22
- package/dist/utils/planningReferences.d.ts +17 -0
- package/dist/utils/planningReferences.js +30 -0
- package/dist/utils/planningReferences.test.d.ts +1 -0
- package/dist/utils/planningReferences.test.js +17 -0
- package/dist/utils/taskActivity.d.ts +9 -0
- package/dist/utils/taskActivity.js +147 -0
- package/dist/utils/taskActivity.test.d.ts +1 -0
- package/dist/utils/taskActivity.test.js +34 -0
- package/dist/utils/taskDraftDefaults.d.ts +21 -0
- package/dist/utils/taskDraftDefaults.js +40 -0
- package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
- package/dist/utils/taskDraftDefaults.test.js +34 -0
- package/dist/utils/taskNormalization.d.ts +25 -0
- package/dist/utils/taskNormalization.js +161 -0
- package/dist/utils/taskNormalization.test.d.ts +1 -0
- package/dist/utils/taskNormalization.test.js +158 -0
- package/dist/utils/taskReferences.d.ts +5 -0
- package/dist/utils/taskReferences.js +11 -0
- package/dist/utils/taskforceApiClient.d.ts +2 -0
- package/dist/utils/taskforceApiClient.js +101 -3
- package/dist/utils/taskforceApiClient.test.d.ts +1 -0
- package/dist/utils/taskforceApiClient.test.js +44 -0
- package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
- package/dist/utils/taxonomyFilterOptions.js +17 -0
- package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
- package/dist/utils/taxonomyFormDefaults.js +28 -0
- package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
- package/dist/utils/taxonomyFormDefaults.test.js +63 -0
- package/dist/utils/taxonomyLifecycle.d.ts +3 -0
- package/dist/utils/taxonomyLifecycle.js +17 -0
- package/dist/utils/taxonomySorting.d.ts +7 -0
- package/dist/utils/taxonomySorting.js +74 -0
- package/dist/utils/theme.d.ts +15 -0
- package/dist/utils/theme.js +23 -0
- package/dist/utils/uiNotice.d.ts +7 -0
- package/dist/utils/uiNotice.js +6 -0
- package/dist/utils/uiNotice.test.d.ts +1 -0
- package/dist/utils/uiNotice.test.js +18 -0
- package/dist/utils/workspaceIdentity.d.ts +14 -0
- package/dist/utils/workspaceIdentity.js +21 -0
- package/dist/utils/workspaceIdentity.test.js +30 -1
- package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
- package/dist/utils/workspaceSyncPresentation.js +21 -0
- package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
- package/dist/utils/workspaceSyncPresentation.test.js +36 -0
- package/package.json +30 -23
- package/scripts/bootstrap-playwright-auth.mjs +155 -0
- package/scripts/check-compat-boundaries.mjs +41 -0
- package/scripts/check-env.ts +9 -2
- package/scripts/debug-login.mjs +32 -0
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/migrate-planning-model.ts +233 -0
- package/scripts/migrate-task-data-assets.mjs +408 -0
- package/scripts/playwright-auth.sh +11 -0
- package/scripts/run-dev-proxy.sh +2 -0
- package/scripts/update-homebrew-formula.sh +2 -2
- package/scripts/with-esbuild.sh +11 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/src/resources/templates/workflows/collaborate.yaml +11 -5
- package/src/resources/templates/workflows/do.yaml +8 -2
- package/src/resources/templates/workflows/evaluate.yaml +13 -7
- package/src/resources/templates/workflows/execute.yaml +40 -23
- package/src/resources/templates/workflows/plan.yaml +15 -8
- package/src/resources/templates/workflows/review.yaml +14 -8
- package/dist/TaskforceScreenshot.d.ts +0 -11
- package/dist/TaskforceScreenshot.js +0 -113
- package/dist/compat/priorityCompat.d.ts +0 -2
- package/dist/compat/priorityCompat.js +0 -24
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
- package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
- package/dist/components/task/TaskKanban 2.d.ts +0 -76
- package/dist/components/task/TaskKanban 2.js +0 -1004
- package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
- package/dist/components/task/TaskScreenshotSection.js +0 -22
- package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
- package/dist/components/views/StandaloneLayout 2.js +0 -1660
- package/dist/core/InitiativeTemplates.test.js +0 -50
- package/dist/core/ParentLifecycle.test.js +0 -98
- package/dist/migrations/clientMigrations.d.ts +0 -9
- package/dist/migrations/clientMigrations.js +0 -74
- package/dist/migrations/flags.d.ts +0 -6
- package/dist/migrations/flags.js +0 -8
- package/dist/migrations/taskMigrations.d.ts +0 -71
- package/dist/migrations/taskMigrations.js +0 -236
- package/dist/migrations/taskMigrations.test.js +0 -34
- package/dist/server/routes.billing.js +0 -991
- package/dist/server/routes.billing.test.js +0 -387
- package/dist/server/routes.sync.integration.test.js +0 -470
- package/dist/server/routes.sync.js +0 -1141
- package/dist/soak/syncSoakHarness.d.ts +0 -6
- package/dist/soak/syncSoakHarness.js +0 -10
- package/dist/ui/assets/index-0W5vyCpn.css +0 -1
- package/dist/ui/assets/index-MCZuNNfn.js +0 -23
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
- package/dist/utils/taskHierarchy.d.ts +0 -4
- package/dist/utils/taskHierarchy.js +0 -69
- package/dist/utils/taskHierarchy.test.js +0 -38
- package/dist/utils/taskParenting.d.ts +0 -3
- package/dist/utils/taskParenting.js +0 -24
- package/dist/utils/taskParenting.test.js +0 -21
- package/dist/utils/taskRelations.d.ts +0 -6
- package/dist/utils/taskRelations.js +0 -24
- package/dist/utils/taskRelations.test.js +0 -34
- package/scripts/check-compat-boundaries.js +0 -48
- /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
- /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
- /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
- /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
- /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
- /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
- /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
- /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
- /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
- /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
- /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
- /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
- /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
- /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
- /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
- /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
|
@@ -1,17 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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'
|
|
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
|
-
|
|
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
|
-
//
|
|
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 [
|
|
75
|
-
const [
|
|
76
|
-
const [
|
|
77
|
-
const [
|
|
78
|
-
const [
|
|
79
|
-
|
|
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
|
|
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
|
|
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 =
|
|
133
|
-
const retryAfterFloor = Number.isFinite(Number(minDelayMs))
|
|
134
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
514
|
+
const remoteTheme = normalizeTaskforceTheme(remote.theme);
|
|
515
|
+
if (remoteTheme) {
|
|
516
|
+
setGlobalTheme(remoteTheme);
|
|
221
517
|
if (themeUseGlobalDefault)
|
|
222
|
-
setCurrentTheme(
|
|
223
|
-
patch.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
|
-
}, [
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
366
|
-
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
|
-
|
|
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
|
-
}, [
|
|
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:
|
|
398
|
-
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
|
-
|
|
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
|
-
}, [
|
|
404
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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(
|
|
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 ===
|
|
886
|
+
if (fingerprint === workspaceSyncMarkdownDocumentsFingerprintRef.current)
|
|
431
887
|
return;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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:
|
|
440
|
-
error:
|
|
895
|
+
workspaceId: currentWorkspaceId,
|
|
896
|
+
error: String(error?.message || error || '')
|
|
441
897
|
});
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
}, [
|
|
1228
|
+
}, [
|
|
1229
|
+
clearWorkspaceRetry,
|
|
1230
|
+
reportSyncEvent,
|
|
1231
|
+
currentWorkspaceId,
|
|
1232
|
+
setAuthBlocked,
|
|
1233
|
+
setIsAuthenticated,
|
|
1234
|
+
checkAuthSession
|
|
1235
|
+
]);
|
|
462
1236
|
const ensureWorkspaceSyncReady = useCallback(async () => {
|
|
463
|
-
|
|
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
|
-
|
|
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 (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
1274
|
+
reportSyncEvent,
|
|
1275
|
+
scheduleWorkspaceSyncRetry,
|
|
1276
|
+
workspaceSyncPhase,
|
|
1277
|
+
persistWorkspaceSyncPatchSafely
|
|
516
1278
|
]);
|
|
517
|
-
|
|
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
|
-
|
|
526
|
-
if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
|
|
1298
|
+
if (shouldSuppressAttachBootstrapPush(signature)) {
|
|
527
1299
|
return false;
|
|
528
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1482
|
+
clearWorkspaceIssue,
|
|
1483
|
+
refreshWorkspaceSyncMarkdownDocumentsSnapshot,
|
|
1484
|
+
refreshWorkspaceSyncAiProfilesSnapshot,
|
|
1485
|
+
refreshWorkspaceSyncAssetsSnapshot,
|
|
1486
|
+
refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
|
|
679
1487
|
ensureWorkspaceSyncReady,
|
|
680
|
-
|
|
1488
|
+
buildWorkspaceSyncPayload,
|
|
1489
|
+
resolveCloudAuthUrl,
|
|
681
1490
|
ensureCloudWorkspaceReadyForSync,
|
|
1491
|
+
shouldSuppressAttachBootstrapPush,
|
|
682
1492
|
handleSyncAuthFailure,
|
|
683
|
-
recordWorkspaceSyncFailure,
|
|
684
1493
|
clearWorkspaceRetry,
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1494
|
+
persistWorkspaceSyncPatch,
|
|
1495
|
+
reportSyncEvent,
|
|
1496
|
+
scheduleWorkspaceSyncRetry,
|
|
1497
|
+
acquireWorkspaceSyncLease,
|
|
1498
|
+
releaseWorkspaceSyncLease,
|
|
1499
|
+
reportWorkspaceWindowContention,
|
|
1500
|
+
workspacePushInFlightRef,
|
|
1501
|
+
workspacePullInFlightRef,
|
|
1502
|
+
persistWorkspaceSyncPatchSafely
|
|
688
1503
|
]);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
744
|
-
bootstrap
|
|
1542
|
+
cursor,
|
|
1543
|
+
bootstrap,
|
|
745
1544
|
ensureCloudWorkspaceReadyForSync,
|
|
746
1545
|
maxPages: 20,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
|
|
848
|
-
setWorkspaceLastWarningMessage(warning);
|
|
849
|
-
logSyncDebug('pull_document_decrypt_partial_failure', {
|
|
1603
|
+
logSyncDebug('pull_document_decrypt_failures', {
|
|
850
1604
|
workspaceId: currentWorkspaceId,
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
968
|
-
|
|
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
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1693
|
+
workspaceSyncPhase,
|
|
1694
|
+
isWorkspaceRetryPending,
|
|
1695
|
+
isAuthenticated,
|
|
1696
|
+
checkAuthSession,
|
|
1697
|
+
clearWorkspaceIssue,
|
|
994
1698
|
ensureWorkspaceSyncReady,
|
|
1699
|
+
resolveCloudAuthUrl,
|
|
1700
|
+
ensureCloudWorkspaceReadyForSync,
|
|
995
1701
|
handleSyncAuthFailure,
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
1737
|
+
void pullWorkspaceChangesFromCloud();
|
|
1036
1738
|
}, 180);
|
|
1037
1739
|
}, [
|
|
1038
1740
|
cloudAuthConfigured,
|
|
1039
1741
|
runtimeMode,
|
|
1040
1742
|
workspaceCloudSyncEnabled,
|
|
1041
|
-
|
|
1743
|
+
workspaceSyncPhase,
|
|
1744
|
+
isWorkspaceRetryPending,
|
|
1042
1745
|
workspacePullInFlightRef,
|
|
1043
|
-
|
|
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
|
-
&&
|
|
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 (
|
|
1116
|
-
return saveWorkspaceCloudSyncSettingsBase(
|
|
1804
|
+
const saveWorkspaceCloudSyncSettings = useCallback(async (settings) => {
|
|
1805
|
+
return saveWorkspaceCloudSyncSettingsBase(settings, {
|
|
1117
1806
|
cloudAuthConfigured,
|
|
1118
1807
|
isAuthenticated,
|
|
1119
|
-
|
|
1120
|
-
ensureCloudWorkspaceReadyForSync,
|
|
1121
|
-
fetchLocalWorkspaceSnapshot
|
|
1808
|
+
ensureCloudWorkspaceReadyForSync
|
|
1122
1809
|
});
|
|
1123
1810
|
}, [
|
|
1811
|
+
saveWorkspaceCloudSyncSettingsBase,
|
|
1124
1812
|
cloudAuthConfigured,
|
|
1125
1813
|
isAuthenticated,
|
|
1126
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1850
|
+
return pulled;
|
|
1151
1851
|
}, [
|
|
1152
1852
|
clearWorkspaceRetry,
|
|
1153
1853
|
cloudAuthConfigured,
|
|
1154
1854
|
runtimeMode,
|
|
1855
|
+
workspaceCloudSyncEnabled,
|
|
1155
1856
|
isAuthenticated,
|
|
1156
1857
|
checkAuthSession,
|
|
1157
|
-
|
|
1858
|
+
workspaceSyncPhase,
|
|
1158
1859
|
buildWorkspaceSyncSignature,
|
|
1860
|
+
shouldSuppressAttachBootstrapPush,
|
|
1159
1861
|
pushWorkspaceChangesToCloud,
|
|
1160
1862
|
pullWorkspaceChangesFromCloud
|
|
1161
1863
|
]);
|
|
1162
1864
|
const resetWorkspaceSyncCursorAndPull = useCallback(async () => {
|
|
1163
|
-
|
|
1164
|
-
if (!
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1203
|
-
},
|
|
1204
|
-
|
|
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
|
-
|
|
2064
|
+
authSessionResolved,
|
|
2065
|
+
isAuthenticated,
|
|
2066
|
+
currentWorkspaceId,
|
|
2067
|
+
refreshWorkspaceSyncMarkdownDocumentsSnapshot,
|
|
2068
|
+
refreshWorkspaceSyncAiProfilesSnapshot,
|
|
2069
|
+
refreshWorkspaceSyncAssetsSnapshot,
|
|
2070
|
+
refreshWorkspaceSyncDocumentReviewSessionsSnapshot,
|
|
2071
|
+
refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot,
|
|
2072
|
+
workspaceDocumentsRefreshIntervalMs
|
|
1210
2073
|
]);
|
|
1211
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
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 (
|
|
2148
|
+
if (isWorkspaceRetryPending())
|
|
1252
2149
|
return;
|
|
1253
2150
|
void pullWorkspaceChangesFromCloud();
|
|
1254
2151
|
const intervalId = window.setInterval(() => {
|
|
1255
2152
|
void pullWorkspaceChangesFromCloud();
|
|
1256
|
-
},
|
|
2153
|
+
}, workspacePullIntervalMs);
|
|
1257
2154
|
return () => window.clearInterval(intervalId);
|
|
1258
2155
|
}, [
|
|
1259
2156
|
cloudAuthConfigured,
|
|
1260
2157
|
runtimeMode,
|
|
1261
2158
|
workspaceCloudSyncEnabled,
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1351
|
-
workspaceLastWarningMessage,
|
|
1352
|
-
setWorkspaceLastWarningMessage,
|
|
2263
|
+
workspaceLastSuccessfulSyncAt,
|
|
1353
2264
|
workspaceCloudSyncEnabled,
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
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
|
}
|