@taskforcehq/taskforce 0.3.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (492) hide show
  1. package/README.md +241 -0
  2. package/dist/Taskforce.d.ts +3 -0
  3. package/dist/Taskforce.js +3 -0
  4. package/dist/Taskforce.module.css +7587 -0
  5. package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
  6. package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
  7. package/dist/TaskforceCore.d.ts +12 -0
  8. package/dist/TaskforceCore.js +729 -0
  9. package/dist/TaskforceCore.test.d.ts +1 -0
  10. package/dist/TaskforceCore.test.js +2132 -0
  11. package/dist/TaskforceScreenshot.d.ts +11 -0
  12. package/dist/TaskforceScreenshot.js +113 -0
  13. package/dist/TaskforceWidget.d.ts +6 -0
  14. package/dist/TaskforceWidget.js +101 -0
  15. package/dist/TaskforceWidget.test.d.ts +1 -0
  16. package/dist/TaskforceWidget.test.js +92 -0
  17. package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
  18. package/dist/UnsavedChangesRelaxed.test.js +16 -0
  19. package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
  20. package/dist/__tests__/localizationCatalogs.test.js +37 -0
  21. package/dist/__tests__/planParser.test.d.ts +1 -0
  22. package/dist/__tests__/planParser.test.js +183 -0
  23. package/dist/__tests__/releaseVersion.test.d.ts +1 -0
  24. package/dist/__tests__/releaseVersion.test.js +32 -0
  25. package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
  26. package/dist/__tests__/structuredCommentValidation.test.js +31 -0
  27. package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
  28. package/dist/__tests__/structuredDocValidation.test.js +30 -0
  29. package/dist/__tests__/templateEngine.test.d.ts +1 -0
  30. package/dist/__tests__/templateEngine.test.js +150 -0
  31. package/dist/__tests__/templates.test.d.ts +1 -0
  32. package/dist/__tests__/templates.test.js +45 -0
  33. package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
  34. package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
  35. package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
  36. package/dist/__tests__/workflowExportMatrix.test.js +113 -0
  37. package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
  38. package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
  39. package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
  40. package/dist/__tests__/workflowGeneration.test.js +12 -0
  41. package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
  42. package/dist/__tests__/workflowSimulation.test.js +113 -0
  43. package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
  44. package/dist/__tests__/workflowV2Policy.test.js +80 -0
  45. package/dist/api.d.ts +30 -0
  46. package/dist/api.js +85 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +217 -0
  49. package/dist/compat/priorityCompat.d.ts +2 -0
  50. package/dist/compat/priorityCompat.js +24 -0
  51. package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
  52. package/dist/components/features/DocumentGenerateModal.js +114 -0
  53. package/dist/components/features/DocumentIndex.d.ts +19 -0
  54. package/dist/components/features/DocumentIndex.js +51 -0
  55. package/dist/components/features/DocumentViewer.d.ts +9 -0
  56. package/dist/components/features/DocumentViewer.js +253 -0
  57. package/dist/components/features/DocumentWorkspace.d.ts +21 -0
  58. package/dist/components/features/DocumentWorkspace.js +88 -0
  59. package/dist/components/features/TaskSettings.d.ts +209 -0
  60. package/dist/components/features/TaskSettings.js +500 -0
  61. package/dist/components/features/TaskSettings.test.d.ts +1 -0
  62. package/dist/components/features/TaskSettings.test.js +308 -0
  63. package/dist/components/features/settings/CategoryManager.d.ts +19 -0
  64. package/dist/components/features/settings/CategoryManager.js +113 -0
  65. package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
  66. package/dist/components/features/settings/TaxonomyEditor.js +98 -0
  67. package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
  68. package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
  69. package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
  70. package/dist/components/features/settings/TaxonomyManager.js +77 -0
  71. package/dist/components/features/settings/TypeManager.d.ts +10 -0
  72. package/dist/components/features/settings/TypeManager.js +50 -0
  73. package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
  74. package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
  75. package/dist/components/task/TaskActionHeader.d.ts +20 -0
  76. package/dist/components/task/TaskActionHeader.js +13 -0
  77. package/dist/components/task/TaskCard.d.ts +47 -0
  78. package/dist/components/task/TaskCard.js +326 -0
  79. package/dist/components/task/TaskCard.test.d.ts +1 -0
  80. package/dist/components/task/TaskCard.test.js +95 -0
  81. package/dist/components/task/TaskContextUpload.d.ts +12 -0
  82. package/dist/components/task/TaskContextUpload.js +393 -0
  83. package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
  84. package/dist/components/task/TaskContextUpload.test.js +189 -0
  85. package/dist/components/task/TaskForm.d.ts +105 -0
  86. package/dist/components/task/TaskForm.js +241 -0
  87. package/dist/components/task/TaskForm.test.d.ts +1 -0
  88. package/dist/components/task/TaskForm.test.js +288 -0
  89. package/dist/components/task/TaskKanban 2.d.ts +76 -0
  90. package/dist/components/task/TaskKanban 2.js +1004 -0
  91. package/dist/components/task/TaskKanban.d.ts +76 -0
  92. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
  93. package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
  94. package/dist/components/task/TaskKanban.js +1026 -0
  95. package/dist/components/task/TaskKanban.test.d.ts +1 -0
  96. package/dist/components/task/TaskKanban.test.js +46 -0
  97. package/dist/components/task/TaskList.d.ts +74 -0
  98. package/dist/components/task/TaskList.js +38 -0
  99. package/dist/components/task/TaskList.test.d.ts +1 -0
  100. package/dist/components/task/TaskList.test.js +270 -0
  101. package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
  102. package/dist/components/task/TaskScreenshotSection.js +22 -0
  103. package/dist/components/ui/LevelSelector.d.ts +13 -0
  104. package/dist/components/ui/LevelSelector.js +25 -0
  105. package/dist/components/ui/LevelSelector.test.d.ts +1 -0
  106. package/dist/components/ui/LevelSelector.test.js +55 -0
  107. package/dist/components/ui/Markdown.d.ts +6 -0
  108. package/dist/components/ui/Markdown.js +94 -0
  109. package/dist/components/ui/Markdown.test.d.ts +1 -0
  110. package/dist/components/ui/Markdown.test.js +38 -0
  111. package/dist/components/ui/Modal.d.ts +16 -0
  112. package/dist/components/ui/Modal.js +49 -0
  113. package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
  114. package/dist/components/ui/MultiSelectFilter.js +71 -0
  115. package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
  116. package/dist/components/ui/MultiSelectFilter.test.js +74 -0
  117. package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
  118. package/dist/components/ui/TaxonomyDropdown.js +145 -0
  119. package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
  120. package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
  121. package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
  122. package/dist/components/views/StandaloneLayout 2.js +1660 -0
  123. package/dist/components/views/StandaloneLayout.d.ts +8 -0
  124. package/dist/components/views/StandaloneLayout.js +2117 -0
  125. package/dist/components/views/WidgetView.d.ts +8 -0
  126. package/dist/components/views/WidgetView.js +221 -0
  127. package/dist/config/envSchema.d.ts +28 -0
  128. package/dist/config/envSchema.js +374 -0
  129. package/dist/config/envSchema.test.d.ts +1 -0
  130. package/dist/config/envSchema.test.js +97 -0
  131. package/dist/config/surfaceBase.d.ts +2 -0
  132. package/dist/config/surfaceBase.js +8 -0
  133. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
  134. package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
  135. package/dist/core/CompletedAt.test.d.ts +1 -0
  136. package/dist/core/CompletedAt.test.js +44 -0
  137. package/dist/core/CreatedBy.test.d.ts +1 -0
  138. package/dist/core/CreatedBy.test.js +60 -0
  139. package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
  140. package/dist/core/EntitlementsPolicy.test.js +145 -0
  141. package/dist/core/InitiativeTemplates.test.d.ts +1 -0
  142. package/dist/core/InitiativeTemplates.test.js +50 -0
  143. package/dist/core/ParentLifecycle.test.d.ts +1 -0
  144. package/dist/core/ParentLifecycle.test.js +98 -0
  145. package/dist/core/ScheduleFields.test.d.ts +1 -0
  146. package/dist/core/ScheduleFields.test.js +197 -0
  147. package/dist/core/SetupState.test.d.ts +1 -0
  148. package/dist/core/SetupState.test.js +113 -0
  149. package/dist/core/SystemAdmin.test.d.ts +1 -0
  150. package/dist/core/SystemAdmin.test.js +295 -0
  151. package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
  152. package/dist/core/TaskTaxonomyValidation.test.js +64 -0
  153. package/dist/core/Taskforce.d.ts +1063 -0
  154. package/dist/core/Taskforce.ids.test.d.ts +1 -0
  155. package/dist/core/Taskforce.ids.test.js +168 -0
  156. package/dist/core/Taskforce.js +5981 -0
  157. package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
  158. package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
  159. package/dist/core/WorkspaceDelete.test.d.ts +1 -0
  160. package/dist/core/WorkspaceDelete.test.js +140 -0
  161. package/dist/core/WorkspacePermissions.test.d.ts +1 -0
  162. package/dist/core/WorkspacePermissions.test.js +151 -0
  163. package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
  164. package/dist/core/WorkspacePlanMode.test.js +104 -0
  165. package/dist/hooks/authGuardPolicy.d.ts +26 -0
  166. package/dist/hooks/authGuardPolicy.js +32 -0
  167. package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
  168. package/dist/hooks/authGuardPolicy.test.js +76 -0
  169. package/dist/hooks/useDraggable.d.ts +12 -0
  170. package/dist/hooks/useDraggable.js +65 -0
  171. package/dist/hooks/useRealtimeSync.d.ts +24 -0
  172. package/dist/hooks/useRealtimeSync.js +275 -0
  173. package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
  174. package/dist/hooks/useRealtimeSync.test.js +284 -0
  175. package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
  176. package/dist/hooks/useSyncOrchestrator.js +1427 -0
  177. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
  178. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
  179. package/dist/hooks/useTaskforce.d.ts +727 -0
  180. package/dist/hooks/useTaskforce.js +4396 -0
  181. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
  182. package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
  183. package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
  184. package/dist/hooks/useWorkspaceSyncController.js +598 -0
  185. package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
  186. package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
  187. package/dist/index.d.ts +12 -0
  188. package/dist/index.js +11 -0
  189. package/dist/localization/index.d.ts +8 -0
  190. package/dist/localization/index.js +81 -0
  191. package/dist/localization/locales/en-US.d.ts +151 -0
  192. package/dist/localization/locales/en-US.js +150 -0
  193. package/dist/localization/locales/es-419.d.ts +2 -0
  194. package/dist/localization/locales/es-419.js +150 -0
  195. package/dist/localization/locales/pt-BR.d.ts +2 -0
  196. package/dist/localization/locales/pt-BR.js +150 -0
  197. package/dist/main.d.ts +1 -0
  198. package/dist/main.js +33 -0
  199. package/dist/mcp/optionResolution.d.ts +14 -0
  200. package/dist/mcp/optionResolution.js +126 -0
  201. package/dist/mcp/optionResolution.test.d.ts +1 -0
  202. package/dist/mcp/optionResolution.test.js +39 -0
  203. package/dist/mcp/server.d.ts +1 -0
  204. package/dist/mcp/server.js +1793 -0
  205. package/dist/mcp/structuredCommentValidation.d.ts +6 -0
  206. package/dist/mcp/structuredCommentValidation.js +56 -0
  207. package/dist/mcp/structuredDocValidation.d.ts +6 -0
  208. package/dist/mcp/structuredDocValidation.js +75 -0
  209. package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
  210. package/dist/migrations/billingSchemaParity.test.js +46 -0
  211. package/dist/migrations/clientMigrations.d.ts +9 -0
  212. package/dist/migrations/clientMigrations.js +74 -0
  213. package/dist/migrations/flags.d.ts +6 -0
  214. package/dist/migrations/flags.js +8 -0
  215. package/dist/migrations/taskMigrations.d.ts +71 -0
  216. package/dist/migrations/taskMigrations.js +236 -0
  217. package/dist/migrations/taskMigrations.test.d.ts +1 -0
  218. package/dist/migrations/taskMigrations.test.js +34 -0
  219. package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
  220. package/dist/migrations/taskSchemaMigrations.js +453 -0
  221. package/dist/plugins/index.d.ts +10 -0
  222. package/dist/plugins/index.js +10 -0
  223. package/dist/plugins/vite.d.ts +19 -0
  224. package/dist/plugins/vite.js +271 -0
  225. package/dist/public/android-chrome-192x192.png +0 -0
  226. package/dist/public/android-chrome-512x512.png +0 -0
  227. package/dist/public/apple-touch-icon.png +0 -0
  228. package/dist/public/favicon-16x16.png +0 -0
  229. package/dist/public/favicon-32x32.png +0 -0
  230. package/dist/public/favicon.ico +0 -0
  231. package/dist/public/favicon.png +0 -0
  232. package/dist/public/site.webmanifest +1 -0
  233. package/dist/public/taskforce.png +0 -0
  234. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  235. package/dist/resources/templates/environments/aider.yaml +14 -0
  236. package/dist/resources/templates/environments/antigravity.yaml +10 -0
  237. package/dist/resources/templates/environments/claude.yaml +10 -0
  238. package/dist/resources/templates/environments/cline.yaml +10 -0
  239. package/dist/resources/templates/environments/codex.yaml +13 -0
  240. package/dist/resources/templates/environments/copilot.yaml +10 -0
  241. package/dist/resources/templates/environments/cursor.yaml +9 -0
  242. package/dist/resources/templates/environments/gemini.yaml +9 -0
  243. package/dist/resources/templates/environments/windsurf.yaml +10 -0
  244. package/dist/resources/templates/workflow-sources/README.md +9 -0
  245. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  246. package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  247. package/dist/resources/templates/workflows/collaborate.yaml +104 -0
  248. package/dist/resources/templates/workflows/do.yaml +82 -0
  249. package/dist/resources/templates/workflows/evaluate.yaml +111 -0
  250. package/dist/resources/templates/workflows/execute.yaml +142 -0
  251. package/dist/resources/templates/workflows/plan.yaml +112 -0
  252. package/dist/resources/templates/workflows/review.yaml +109 -0
  253. package/dist/runtime/capabilities.d.ts +8 -0
  254. package/dist/runtime/capabilities.js +16 -0
  255. package/dist/security/virusScan.d.ts +16 -0
  256. package/dist/security/virusScan.js +83 -0
  257. package/dist/server/auth.d.ts +51 -0
  258. package/dist/server/auth.js +266 -0
  259. package/dist/server/auth.test.d.ts +1 -0
  260. package/dist/server/auth.test.js +111 -0
  261. package/dist/server/buildInfo.d.ts +7 -0
  262. package/dist/server/buildInfo.js +100 -0
  263. package/dist/server/buildInfo.test.d.ts +1 -0
  264. package/dist/server/buildInfo.test.js +29 -0
  265. package/dist/server/cors.d.ts +7 -0
  266. package/dist/server/cors.js +76 -0
  267. package/dist/server/cors.test.d.ts +1 -0
  268. package/dist/server/cors.test.js +71 -0
  269. package/dist/server/email.d.ts +28 -0
  270. package/dist/server/email.js +138 -0
  271. package/dist/server/email.test.d.ts +1 -0
  272. package/dist/server/email.test.js +113 -0
  273. package/dist/server/index.cookieProxy.test.d.ts +1 -0
  274. package/dist/server/index.cookieProxy.test.js +19 -0
  275. package/dist/server/index.d.ts +17 -0
  276. package/dist/server/index.js +757 -0
  277. package/dist/server/index.rateLimit.test.d.ts +1 -0
  278. package/dist/server/index.rateLimit.test.js +13 -0
  279. package/dist/server/index.test.d.ts +1 -0
  280. package/dist/server/index.test.js +19 -0
  281. package/dist/server/rateLimit.d.ts +29 -0
  282. package/dist/server/rateLimit.js +99 -0
  283. package/dist/server/rateLimit.test.d.ts +1 -0
  284. package/dist/server/rateLimit.test.js +107 -0
  285. package/dist/server/realtimeSync.auth.test.d.ts +1 -0
  286. package/dist/server/realtimeSync.auth.test.js +62 -0
  287. package/dist/server/realtimeSync.replay.test.d.ts +1 -0
  288. package/dist/server/realtimeSync.replay.test.js +192 -0
  289. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
  290. package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
  291. package/dist/server/realtimeSyncWs.d.ts +30 -0
  292. package/dist/server/realtimeSyncWs.js +257 -0
  293. package/dist/server/routes.billing.d.ts +15 -0
  294. package/dist/server/routes.billing.js +991 -0
  295. package/dist/server/routes.billing.test.d.ts +1 -0
  296. package/dist/server/routes.billing.test.js +387 -0
  297. package/dist/server/routes.d.ts +72 -0
  298. package/dist/server/routes.js +4820 -0
  299. package/dist/server/routes.sync.d.ts +17 -0
  300. package/dist/server/routes.sync.integration.test.d.ts +1 -0
  301. package/dist/server/routes.sync.integration.test.js +470 -0
  302. package/dist/server/routes.sync.js +1141 -0
  303. package/dist/server/routes.test.d.ts +1 -0
  304. package/dist/server/routes.test.js +4945 -0
  305. package/dist/server/runtimeMode.d.ts +4 -0
  306. package/dist/server/runtimeMode.js +25 -0
  307. package/dist/server/runtimeMode.test.d.ts +1 -0
  308. package/dist/server/runtimeMode.test.js +26 -0
  309. package/dist/server/securityHeaders.d.ts +3 -0
  310. package/dist/server/securityHeaders.js +25 -0
  311. package/dist/server/securityHeaders.test.d.ts +1 -0
  312. package/dist/server/securityHeaders.test.js +32 -0
  313. package/dist/server/smtpTransport.d.ts +21 -0
  314. package/dist/server/smtpTransport.js +11 -0
  315. package/dist/server/start.d.ts +1 -0
  316. package/dist/server/start.js +17 -0
  317. package/dist/services/formatTransformers.d.ts +29 -0
  318. package/dist/services/formatTransformers.js +56 -0
  319. package/dist/services/templateEngine.d.ts +46 -0
  320. package/dist/services/templateEngine.js +148 -0
  321. package/dist/shared/runtimeContract.d.ts +12 -0
  322. package/dist/shared/runtimeContract.js +23 -0
  323. package/dist/shared/runtimeContract.test.d.ts +1 -0
  324. package/dist/shared/runtimeContract.test.js +23 -0
  325. package/dist/soak/preflight.cli.d.ts +1 -0
  326. package/dist/soak/preflight.cli.js +28 -0
  327. package/dist/soak/preflight.d.ts +26 -0
  328. package/dist/soak/preflight.js +113 -0
  329. package/dist/soak/preflight.test.d.ts +1 -0
  330. package/dist/soak/preflight.test.js +39 -0
  331. package/dist/soak/syncSoakHarness.d.ts +6 -0
  332. package/dist/soak/syncSoakHarness.js +10 -0
  333. package/dist/storage/createDatabaseAdapter.d.ts +2 -0
  334. package/dist/storage/createDatabaseAdapter.js +9 -0
  335. package/dist/storage/databaseAdapter.d.ts +22 -0
  336. package/dist/storage/databaseAdapter.js +1 -0
  337. package/dist/storage/documentIntegrity.d.ts +28 -0
  338. package/dist/storage/documentIntegrity.js +103 -0
  339. package/dist/storage/documentPurge.d.ts +14 -0
  340. package/dist/storage/documentPurge.js +46 -0
  341. package/dist/storage/documentRegistry.d.ts +39 -0
  342. package/dist/storage/documentRegistry.js +115 -0
  343. package/dist/storage/integritySchedule.d.ts +17 -0
  344. package/dist/storage/integritySchedule.js +62 -0
  345. package/dist/storage/objectStorage.d.ts +20 -0
  346. package/dist/storage/objectStorage.js +15 -0
  347. package/dist/storage/objectStorageClient.d.ts +18 -0
  348. package/dist/storage/objectStorageClient.js +301 -0
  349. package/dist/storage/objectStorageConfig.d.ts +2 -0
  350. package/dist/storage/objectStorageConfig.js +45 -0
  351. package/dist/storage/postgresAdapter.d.ts +20 -0
  352. package/dist/storage/postgresAdapter.js +204 -0
  353. package/dist/storage/postgresWorker.d.ts +1 -0
  354. package/dist/storage/postgresWorker.js +110 -0
  355. package/dist/storage/providerConfig.d.ts +6 -0
  356. package/dist/storage/providerConfig.js +11 -0
  357. package/dist/storage/sqliteAdapter.d.ts +14 -0
  358. package/dist/storage/sqliteAdapter.js +28 -0
  359. package/dist/storage/taskAssetStore.d.ts +56 -0
  360. package/dist/storage/taskAssetStore.js +297 -0
  361. package/dist/sync/cloudSyncApi.d.ts +47 -0
  362. package/dist/sync/cloudSyncApi.js +138 -0
  363. package/dist/sync/cloudSyncApi.test.d.ts +1 -0
  364. package/dist/sync/cloudSyncApi.test.js +54 -0
  365. package/dist/sync/encryptionService.d.ts +11 -0
  366. package/dist/sync/encryptionService.js +61 -0
  367. package/dist/sync/encryptionService.test.d.ts +1 -0
  368. package/dist/sync/encryptionService.test.js +52 -0
  369. package/dist/sync/realtimeFeature.d.ts +6 -0
  370. package/dist/sync/realtimeFeature.js +20 -0
  371. package/dist/sync/realtimeFeature.test.d.ts +1 -0
  372. package/dist/sync/realtimeFeature.test.js +30 -0
  373. package/dist/sync/realtimeReconcile.d.ts +30 -0
  374. package/dist/sync/realtimeReconcile.js +90 -0
  375. package/dist/sync/realtimeReconcile.test.d.ts +1 -0
  376. package/dist/sync/realtimeReconcile.test.js +43 -0
  377. package/dist/sync/syncService.d.ts +96 -0
  378. package/dist/sync/syncService.js +423 -0
  379. package/dist/sync/syncService.test.d.ts +1 -0
  380. package/dist/sync/syncService.test.js +484 -0
  381. package/dist/sync/workspaceSyncModel.d.ts +61 -0
  382. package/dist/sync/workspaceSyncModel.js +136 -0
  383. package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
  384. package/dist/sync/workspaceSyncModel.test.js +73 -0
  385. package/dist/test/setup.d.ts +1 -0
  386. package/dist/test/setup.js +15 -0
  387. package/dist/types.d.ts +157 -0
  388. package/dist/types.js +41 -0
  389. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  390. package/dist/ui/assets/index-0W5vyCpn.css +1 -0
  391. package/dist/ui/assets/index-MCZuNNfn.js +23 -0
  392. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  393. package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
  394. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
  395. package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
  396. package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
  397. package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
  398. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
  399. package/dist/ui/branding/logos/logo_black_01.png +0 -0
  400. package/dist/ui/branding/logos/logo_black_02.png +0 -0
  401. package/dist/ui/branding/logos/logo_white_01.png +0 -0
  402. package/dist/ui/branding/logos/logo_white_02.png +0 -0
  403. package/dist/ui/favicon/android-chrome-192x192.png +0 -0
  404. package/dist/ui/favicon/android-chrome-512x512.png +0 -0
  405. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  406. package/dist/ui/favicon/favicon-16x16.png +0 -0
  407. package/dist/ui/favicon/favicon-32x32.png +0 -0
  408. package/dist/ui/favicon/favicon.ico +0 -0
  409. package/dist/ui/favicon/favicon.png +0 -0
  410. package/dist/ui/favicon/site.webmanifest +1 -0
  411. package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
  412. package/dist/ui/images/taskforce.png +0 -0
  413. package/dist/ui/index.html +19 -0
  414. package/dist/utils/checklistMarkdown.d.ts +9 -0
  415. package/dist/utils/checklistMarkdown.js +46 -0
  416. package/dist/utils/constants.d.ts +21 -0
  417. package/dist/utils/constants.icons.test.d.ts +1 -0
  418. package/dist/utils/constants.icons.test.js +40 -0
  419. package/dist/utils/constants.js +100 -0
  420. package/dist/utils/contextFiles.d.ts +8 -0
  421. package/dist/utils/contextFiles.js +54 -0
  422. package/dist/utils/formatting.d.ts +5 -0
  423. package/dist/utils/formatting.js +30 -0
  424. package/dist/utils/planParser.d.ts +22 -0
  425. package/dist/utils/planParser.js +151 -0
  426. package/dist/utils/taskDragZones.d.ts +8 -0
  427. package/dist/utils/taskDragZones.js +14 -0
  428. package/dist/utils/taskDragZones.test.d.ts +1 -0
  429. package/dist/utils/taskDragZones.test.js +14 -0
  430. package/dist/utils/taskHierarchy.d.ts +4 -0
  431. package/dist/utils/taskHierarchy.js +69 -0
  432. package/dist/utils/taskHierarchy.test.d.ts +1 -0
  433. package/dist/utils/taskHierarchy.test.js +38 -0
  434. package/dist/utils/taskParenting.d.ts +3 -0
  435. package/dist/utils/taskParenting.js +24 -0
  436. package/dist/utils/taskParenting.test.d.ts +1 -0
  437. package/dist/utils/taskParenting.test.js +21 -0
  438. package/dist/utils/taskRelations.d.ts +6 -0
  439. package/dist/utils/taskRelations.js +24 -0
  440. package/dist/utils/taskRelations.test.d.ts +1 -0
  441. package/dist/utils/taskRelations.test.js +34 -0
  442. package/dist/utils/taskUtils.d.ts +2 -0
  443. package/dist/utils/taskUtils.js +20 -0
  444. package/dist/utils/taskforceApiClient.d.ts +7 -0
  445. package/dist/utils/taskforceApiClient.js +42 -0
  446. package/dist/utils/workspaceIdentity.d.ts +8 -0
  447. package/dist/utils/workspaceIdentity.js +47 -0
  448. package/dist/utils/workspaceIdentity.test.d.ts +1 -0
  449. package/dist/utils/workspaceIdentity.test.js +32 -0
  450. package/dist/utils/workspaceLifecycle.d.ts +26 -0
  451. package/dist/utils/workspaceLifecycle.js +69 -0
  452. package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
  453. package/dist/utils/workspaceLifecycle.test.js +64 -0
  454. package/package.json +133 -0
  455. package/scripts/check-compat-boundaries.js +48 -0
  456. package/scripts/check-env.ts +24 -0
  457. package/scripts/check-localization-strings.mjs +49 -0
  458. package/scripts/check-runtime-boundaries.mjs +44 -0
  459. package/scripts/debug-global.mjs +34 -0
  460. package/scripts/debug-server.mjs +36 -0
  461. package/scripts/export-sqlite-to-postgres.mjs +125 -0
  462. package/scripts/generate-env-example.ts +8 -0
  463. package/scripts/generate-workflow-templates.mjs +134 -0
  464. package/scripts/mcp-stdio-adapter.mjs +160 -0
  465. package/scripts/postinstall.js +60 -0
  466. package/scripts/release-version-lib.mjs +88 -0
  467. package/scripts/release-version.mjs +72 -0
  468. package/scripts/run-dev-proxy.sh +49 -0
  469. package/scripts/run-e2e-realtime.sh +16 -0
  470. package/scripts/run-smoke.sh +23 -0
  471. package/scripts/run-soak.sh +29 -0
  472. package/scripts/test-global.js +143 -0
  473. package/scripts/update-homebrew-formula.sh +38 -0
  474. package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  475. package/src/resources/templates/environments/aider.yaml +14 -0
  476. package/src/resources/templates/environments/antigravity.yaml +10 -0
  477. package/src/resources/templates/environments/claude.yaml +10 -0
  478. package/src/resources/templates/environments/cline.yaml +10 -0
  479. package/src/resources/templates/environments/codex.yaml +13 -0
  480. package/src/resources/templates/environments/copilot.yaml +10 -0
  481. package/src/resources/templates/environments/cursor.yaml +9 -0
  482. package/src/resources/templates/environments/gemini.yaml +9 -0
  483. package/src/resources/templates/environments/windsurf.yaml +10 -0
  484. package/src/resources/templates/workflow-sources/README.md +9 -0
  485. package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  486. package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  487. package/src/resources/templates/workflows/collaborate.yaml +104 -0
  488. package/src/resources/templates/workflows/do.yaml +82 -0
  489. package/src/resources/templates/workflows/evaluate.yaml +111 -0
  490. package/src/resources/templates/workflows/execute.yaml +142 -0
  491. package/src/resources/templates/workflows/plan.yaml +112 -0
  492. package/src/resources/templates/workflows/review.yaml +109 -0
@@ -0,0 +1,1427 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { calculateWorkspaceRetryDelayMs, ensureCloudWorkspaceReadyForSyncService, runWorkspacePullSyncService, runWorkspacePushSyncService, syncUserGlobalSettingsService } from '../sync/syncService';
3
+ import { buildWorkspaceSyncPayloadModel, buildWorkspaceSyncSignatureModel } from '../sync/workspaceSyncModel';
4
+ import { useWorkspaceSyncController } from './useWorkspaceSyncController';
5
+ import { useRealtimeSync } from './useRealtimeSync';
6
+ import { setLocale as setAppLocale } from '../localization';
7
+ // ---------------------------------------------------------------------------
8
+ // Module-level constants & helpers (moved from useTaskforce)
9
+ // ---------------------------------------------------------------------------
10
+ const USER_GLOBAL_SYNC_META_KEY = 'taskforce.userGlobalSyncMeta.v1';
11
+ const SYNC_DEBUG_LOG_KEY = 'taskforce.syncDebug.v1';
12
+ const WORKSPACE_HYDRATION_PUSH_BLOCK_WARNING = 'Local changes queued — waiting for initial cloud pull before upload.';
13
+ const WORKSPACE_HYDRATION_PUSH_BLOCK_ESCAPE_MS = 45_000;
14
+ const WORKSPACE_HYDRATION_PUSH_BLOCK_RECOVERY_COOLDOWN_MS = 15_000;
15
+ function isSyncDebugLoggingEnabled() {
16
+ if (typeof window === 'undefined')
17
+ return false;
18
+ try {
19
+ const raw = String(window.localStorage.getItem(SYNC_DEBUG_LOG_KEY) || '').trim().toLowerCase();
20
+ return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function logSyncDebug(event, details) {
27
+ if (!isSyncDebugLoggingEnabled())
28
+ return;
29
+ const payload = details && typeof details === 'object' ? details : {};
30
+ console.info('[Taskforce Sync]', event, payload);
31
+ }
32
+ function readUserGlobalSyncMeta() {
33
+ if (typeof window === 'undefined')
34
+ return {};
35
+ try {
36
+ const raw = window.localStorage.getItem(USER_GLOBAL_SYNC_META_KEY);
37
+ if (!raw)
38
+ return {};
39
+ const parsed = JSON.parse(raw);
40
+ return parsed && typeof parsed === 'object' ? parsed : {};
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
46
+ function writeUserGlobalSyncMeta(next) {
47
+ if (typeof window === 'undefined')
48
+ return;
49
+ try {
50
+ window.localStorage.setItem(USER_GLOBAL_SYNC_META_KEY, JSON.stringify(next));
51
+ }
52
+ catch {
53
+ // best-effort only
54
+ }
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Hook
58
+ // ---------------------------------------------------------------------------
59
+ export function useSyncOrchestrator(input) {
60
+ const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, taxonomies, setupState, globalTheme, locale, globalWeekStartsOn, themeUseGlobalDefault, setTasks, setArchivedTasks, setAuthBlocked, setIsAuthenticated, checkAuthSession, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn } = input;
61
+ // -----------------------------------------------------------------------
62
+ // Sync state
63
+ // -----------------------------------------------------------------------
64
+ const [userGlobalSyncStatus, setUserGlobalSyncStatus] = useState('disconnected');
65
+ const [userGlobalLastSyncedAt, setUserGlobalLastSyncedAt] = useState(null);
66
+ const [userGlobalSyncPendingChanges, setUserGlobalSyncPendingChanges] = useState(0);
67
+ const [userGlobalSyncError, setUserGlobalSyncError] = useState(null);
68
+ const [workspaceLastPullAt, setWorkspaceLastPullAt] = useState(null);
69
+ const [workspaceLastPushAt, setWorkspaceLastPushAt] = useState(null);
70
+ const [workspaceLastErrorAt, setWorkspaceLastErrorAt] = useState(null);
71
+ const [workspaceLastErrorMessage, setWorkspaceLastErrorMessage] = useState(null);
72
+ const [workspaceLastWarningMessage, setWorkspaceLastWarningMessage] = useState(null);
73
+ const [workspaceCloudSyncEnabled, setWorkspaceCloudSyncEnabled] = useState(false);
74
+ const [workspaceSyncSourceOfTruth, setWorkspaceSyncSourceOfTruth] = useState(null);
75
+ const [workspaceSyncOnboardingCompleted, setWorkspaceSyncOnboardingCompleted] = useState(false);
76
+ const [workspaceSyncReady, setWorkspaceSyncReady] = useState(false);
77
+ const [workspaceCloudHydrated, setWorkspaceCloudHydrated] = useState(false);
78
+ const [workspaceSyncDocuments, setWorkspaceSyncDocuments] = useState([]);
79
+ const [workspaceSyncDocumentsFingerprint, setWorkspaceSyncDocumentsFingerprint] = useState('');
80
+ const [realtimeConnectionState, setRealtimeConnectionState] = useState('degraded-fallback');
81
+ const [realtimeTelemetry, setRealtimeTelemetry] = useState({
82
+ accepted: 0,
83
+ duplicateDiscarded: 0,
84
+ outOfOrderDiscarded: 0,
85
+ invalidDiscarded: 0,
86
+ recoveryTriggered: 0
87
+ });
88
+ // -----------------------------------------------------------------------
89
+ // Sync refs
90
+ // -----------------------------------------------------------------------
91
+ const userSettingsSyncInFlightRef = useRef(false);
92
+ const userSettingsRetryTimeoutRef = useRef(null);
93
+ const userSettingsRetryAttemptRef = useRef(0);
94
+ const applyingRemoteUserSettingsRef = useRef(false);
95
+ const initialUserSettingsSyncCompletedRef = useRef(new Set());
96
+ const lastUserGlobalSyncSignatureRef = useRef('');
97
+ const workspaceSyncDocumentsFingerprintRef = useRef('');
98
+ const syncInFlightRef = useRef(false);
99
+ const workspacePushBlockedSinceRef = useRef(new Map());
100
+ const workspacePushBlockedRecoveryAtRef = useRef(new Map());
101
+ const realtimePullDebounceRef = useRef(null);
102
+ const realtimeSuppressedEventIdsRef = useRef(new Set());
103
+ const realtimeSuppressedEventQueueRef = useRef([]);
104
+ const realtimeTelemetryReportedAtRef = useRef(0);
105
+ const realtimeTelemetrySnapshotRef = useRef({
106
+ accepted: 0,
107
+ duplicateDiscarded: 0,
108
+ outOfOrderDiscarded: 0,
109
+ invalidDiscarded: 0,
110
+ recoveryTriggered: 0
111
+ });
112
+ // Stable ref so ensureWorkspaceSyncReady can schedule a retry via pullFromCloud
113
+ // without creating a forward-reference dependency cycle.
114
+ const pullFromCloudRef = useRef(() => Promise.resolve(false));
115
+ // Mirror of workspaceSyncOnboardingCompleted state — kept as a ref so the
116
+ // pullWorkspaceChangesFromCloud callback can read the current value without
117
+ // needing to be re-created every time the state changes (stale closure guard).
118
+ const onboardingCompletedRef = useRef(false);
119
+ const syncUserGlobalSettingsRef = useRef(async () => { });
120
+ const clearUserSettingsRetry = useCallback(() => {
121
+ if (userSettingsRetryTimeoutRef.current !== null) {
122
+ window.clearTimeout(userSettingsRetryTimeoutRef.current);
123
+ userSettingsRetryTimeoutRef.current = null;
124
+ }
125
+ }, []);
126
+ const scheduleUserSettingsRetry = useCallback((minDelayMs) => {
127
+ if (typeof window === 'undefined')
128
+ return;
129
+ clearUserSettingsRetry();
130
+ const nextAttempt = Math.max(1, Math.floor(userSettingsRetryAttemptRef.current) + 1);
131
+ userSettingsRetryAttemptRef.current = nextAttempt;
132
+ const computedDelay = calculateWorkspaceRetryDelayMs(nextAttempt);
133
+ const retryAfterFloor = Number.isFinite(Number(minDelayMs)) ? Math.max(0, Math.floor(Number(minDelayMs))) : 0;
134
+ const delayMs = Math.max(computedDelay, retryAfterFloor);
135
+ userSettingsRetryTimeoutRef.current = window.setTimeout(() => {
136
+ userSettingsRetryTimeoutRef.current = null;
137
+ void syncUserGlobalSettingsRef.current({ preferCloudOnFirstSync: false });
138
+ }, delayMs);
139
+ }, [clearUserSettingsRetry]);
140
+ // -----------------------------------------------------------------------
141
+ // Reset document fingerprint and push watermarks when workspace changes
142
+ // -----------------------------------------------------------------------
143
+ useEffect(() => {
144
+ workspaceSyncDocumentsFingerprintRef.current = '';
145
+ setWorkspaceSyncDocuments([]);
146
+ setWorkspaceSyncDocumentsFingerprint('');
147
+ setWorkspaceLastWarningMessage(null);
148
+ workspacePushBlockedSinceRef.current.delete(currentWorkspaceId);
149
+ workspacePushBlockedRecoveryAtRef.current.delete(currentWorkspaceId);
150
+ workspaceLastPushedWatermarksRef.current = new Map();
151
+ workspaceLastPushedDocumentPathsRef.current = new Set();
152
+ workspaceLastPushedDocumentWatermarksRef.current = new Map();
153
+ realtimeSuppressedEventIdsRef.current = new Set();
154
+ realtimeSuppressedEventQueueRef.current = [];
155
+ }, [currentWorkspaceId]);
156
+ // -----------------------------------------------------------------------
157
+ // useWorkspaceSyncController
158
+ // -----------------------------------------------------------------------
159
+ const handleFailureRecorded = useCallback(() => {
160
+ setWorkspaceLastErrorAt(new Date().toISOString());
161
+ }, []);
162
+ const { workspaceRetryAt, isWorkspacePullRetryPending, workspaceBootstrapSyncInProgress, setWorkspaceBootstrapSyncInProgress, workspaceBootstrapSyncPages, setWorkspaceBootstrapSyncPages, workspaceBootstrapSyncAppliedChanges, setWorkspaceBootstrapSyncAppliedChanges, workspaceSyncFailureCount, workspaceSyncRetryScheduledCount, workspaceSyncAuthFailureCount, workspaceSyncLastFailureSource, workspaceSyncLastFailureStatusCode, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, workspacePullBootstrapDoneRef, workspaceBackfillForcedRef, workspaceSyncReadyRef, workspaceCloudHydratedRef, clearWorkspaceRetry, scheduleWorkspacePullRetry, isWorkspacePullCooldownActive, markWorkspacePullCooldown, tryAcquireWorkspacePullLease, releaseWorkspacePullLease, recordWorkspaceSyncFailure, incrementWorkspaceSyncAuthFailure, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase } = useWorkspaceSyncController({
163
+ currentWorkspaceId,
164
+ setWorkspaceSyncReady,
165
+ setWorkspaceCloudHydrated,
166
+ onFailureRecorded: handleFailureRecorded,
167
+ setWorkspaceCloudSyncEnabled,
168
+ setWorkspaceSyncSourceOfTruth,
169
+ setWorkspaceSyncOnboardingCompleted,
170
+ setUserGlobalLastSyncedAt,
171
+ setWorkspaceLastPullAt,
172
+ setWorkspaceLastPushAt
173
+ });
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
+ const workspaceLastPushedWatermarksRef = useRef(new Map());
178
+ // Tracks document paths present at last successful push so removed docs can emit
179
+ // document-delete operations on the next payload build.
180
+ const workspaceLastPushedDocumentPathsRef = useRef(new Set());
181
+ // Tracks last pushed document updatedAt per path to reduce unchanged document-upsert churn.
182
+ const workspaceLastPushedDocumentWatermarksRef = useRef(new Map());
183
+ // User global settings sync helpers
184
+ // -----------------------------------------------------------------------
185
+ const getLocalUserSyncUpdatedAt = useCallback((userId) => {
186
+ const key = String(userId || '').trim();
187
+ if (!key)
188
+ return null;
189
+ const meta = readUserGlobalSyncMeta();
190
+ const value = meta[key]?.updatedAt;
191
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
192
+ }, []);
193
+ const setLocalUserSyncUpdatedAt = useCallback((userId, updatedAt) => {
194
+ const key = String(userId || '').trim();
195
+ const ts = String(updatedAt || '').trim();
196
+ if (!key || !ts)
197
+ return;
198
+ const meta = readUserGlobalSyncMeta();
199
+ meta[key] = { updatedAt: ts };
200
+ writeUserGlobalSyncMeta(meta);
201
+ }, []);
202
+ const buildUserGlobalSyncPayload = useCallback(() => {
203
+ const mode = setupState?.mode === 'operations' ? 'operations' : 'core';
204
+ return {
205
+ theme: globalTheme,
206
+ operatingMode: mode,
207
+ localization: {
208
+ locale,
209
+ weekStartsOn: globalWeekStartsOn
210
+ }
211
+ };
212
+ }, [globalTheme, setupState?.mode, locale, globalWeekStartsOn]);
213
+ const applyRemoteUserGlobalSyncPayload = useCallback(async (remote) => {
214
+ if (!remote || typeof remote !== 'object')
215
+ return;
216
+ applyingRemoteUserSettingsRef.current = true;
217
+ try {
218
+ const patch = {};
219
+ if (remote.theme === 'dark' || remote.theme === 'light' || remote.theme === 'midnight') {
220
+ setGlobalTheme(remote.theme);
221
+ if (themeUseGlobalDefault)
222
+ setCurrentTheme(remote.theme);
223
+ patch.theme = remote.theme;
224
+ }
225
+ if (remote.operatingMode === 'core' || remote.operatingMode === 'operations') {
226
+ setSetupState((prev) => prev ? { ...prev, mode: remote.operatingMode } : prev);
227
+ patch.setup = { mode: remote.operatingMode };
228
+ }
229
+ if (remote.localization && typeof remote.localization === 'object') {
230
+ const localizationPatch = {};
231
+ if (typeof remote.localization.locale === 'string' && remote.localization.locale.trim().length > 0) {
232
+ const applied = setAppLocale(remote.localization.locale.trim());
233
+ setLocale(applied);
234
+ }
235
+ const weekStartsOn = String(remote.localization.weekStartsOn || '').trim().toLowerCase();
236
+ if (weekStartsOn === 'sunday' || weekStartsOn === 'monday') {
237
+ setGlobalWeekStartsOn(weekStartsOn);
238
+ localizationPatch.weekStartsOn = weekStartsOn;
239
+ }
240
+ if (Object.keys(localizationPatch).length > 0) {
241
+ patch.schedulePreferences = localizationPatch;
242
+ }
243
+ }
244
+ if (Object.keys(patch).length > 0) {
245
+ await fetch('/api/taskforce/global-settings', {
246
+ method: 'POST',
247
+ headers: { 'Content-Type': 'application/json' },
248
+ credentials: 'include',
249
+ body: JSON.stringify(patch)
250
+ });
251
+ }
252
+ }
253
+ finally {
254
+ applyingRemoteUserSettingsRef.current = false;
255
+ }
256
+ }, [themeUseGlobalDefault, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn]);
257
+ const syncUserGlobalSettings = useCallback(async (options) => {
258
+ const preferCloudOnFirstSync = options?.preferCloudOnFirstSync !== false;
259
+ if (!cloudAuthConfigured)
260
+ return;
261
+ if (runtimeMode !== 'local' || !isAuthenticated) {
262
+ clearUserSettingsRetry();
263
+ userSettingsRetryAttemptRef.current = 0;
264
+ setUserGlobalSyncStatus('disconnected');
265
+ return;
266
+ }
267
+ const userId = String(authUserId || '').trim();
268
+ if (!userId || userId === 'anonymous') {
269
+ clearUserSettingsRetry();
270
+ userSettingsRetryAttemptRef.current = 0;
271
+ setUserGlobalSyncStatus('disconnected');
272
+ return;
273
+ }
274
+ if (userSettingsSyncInFlightRef.current)
275
+ return;
276
+ setUserGlobalSyncStatus('syncing');
277
+ setUserGlobalSyncError(null);
278
+ userSettingsSyncInFlightRef.current = true;
279
+ try {
280
+ const syncResult = await syncUserGlobalSettingsService({
281
+ resolveCloudAuthUrl,
282
+ userId,
283
+ preferCloudOnFirstSync,
284
+ getLocalUpdatedAt: getLocalUserSyncUpdatedAt,
285
+ buildLocalSettings: buildUserGlobalSyncPayload,
286
+ applyRemoteSettings: applyRemoteUserGlobalSyncPayload
287
+ });
288
+ if (!syncResult.success) {
289
+ setUserGlobalSyncStatus('error');
290
+ if (syncResult.transient) {
291
+ setUserGlobalSyncError(syncResult.error || 'Sync failed temporarily. Retrying automatically.');
292
+ scheduleUserSettingsRetry(syncResult.retryAfterMs);
293
+ }
294
+ else {
295
+ clearUserSettingsRetry();
296
+ userSettingsRetryAttemptRef.current = 0;
297
+ setUserGlobalSyncError(syncResult.error || 'Sync failed.');
298
+ }
299
+ return;
300
+ }
301
+ clearUserSettingsRetry();
302
+ userSettingsRetryAttemptRef.current = 0;
303
+ const syncedAt = String(syncResult.updatedAt || '').trim() || new Date().toISOString();
304
+ setLocalUserSyncUpdatedAt(userId, syncedAt);
305
+ setUserGlobalLastSyncedAt(syncedAt);
306
+ setUserGlobalSyncPendingChanges(0);
307
+ setUserGlobalSyncStatus('idle');
308
+ setUserGlobalSyncError(null);
309
+ initialUserSettingsSyncCompletedRef.current.add(userId);
310
+ if (syncResult.mode === 'pulled') {
311
+ lastUserGlobalSyncSignatureRef.current = '';
312
+ }
313
+ }
314
+ catch {
315
+ setUserGlobalSyncStatus('error');
316
+ setUserGlobalSyncError('Sync failed temporarily. Retrying automatically.');
317
+ scheduleUserSettingsRetry();
318
+ }
319
+ finally {
320
+ userSettingsSyncInFlightRef.current = false;
321
+ }
322
+ }, [
323
+ runtimeMode,
324
+ isAuthenticated,
325
+ authUserId,
326
+ getLocalUserSyncUpdatedAt,
327
+ setLocalUserSyncUpdatedAt,
328
+ applyRemoteUserGlobalSyncPayload,
329
+ buildUserGlobalSyncPayload,
330
+ resolveCloudAuthUrl,
331
+ cloudAuthConfigured,
332
+ clearUserSettingsRetry,
333
+ scheduleUserSettingsRetry
334
+ ]);
335
+ useEffect(() => {
336
+ syncUserGlobalSettingsRef.current = syncUserGlobalSettings;
337
+ }, [syncUserGlobalSettings]);
338
+ useEffect(() => {
339
+ return () => {
340
+ clearUserSettingsRetry();
341
+ };
342
+ }, [clearUserSettingsRetry]);
343
+ // -----------------------------------------------------------------------
344
+ // Workspace sync helpers
345
+ // -----------------------------------------------------------------------
346
+ const ensureCloudWorkspaceReadyForSync = useCallback(async () => {
347
+ return ensureCloudWorkspaceReadyForSyncService({
348
+ cloudAuthConfigured,
349
+ runtimeMode,
350
+ isAuthenticated,
351
+ resolveCloudAuthUrl,
352
+ workspaceId: currentWorkspaceId,
353
+ workspaceName: projectName || currentWorkspaceId
354
+ });
355
+ }, [
356
+ cloudAuthConfigured,
357
+ runtimeMode,
358
+ isAuthenticated,
359
+ resolveCloudAuthUrl,
360
+ currentWorkspaceId,
361
+ projectName
362
+ ]);
363
+ const buildWorkspaceSyncPayload = useCallback(() => {
364
+ return buildWorkspaceSyncPayloadModel({
365
+ tasks: tasks || [],
366
+ archivedTasks: archivedTasks || [],
367
+ lastPushedTaskIds: workspaceLastPushedTaskIdsRef.current,
368
+ pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
369
+ taxonomies: taxonomies || [],
370
+ documents: workspaceSyncDocuments || [],
371
+ lastPushedDocumentPaths: workspaceLastPushedDocumentPathsRef.current,
372
+ lastPushedDocumentWatermarks: workspaceLastPushedDocumentWatermarksRef.current,
373
+ lastPushedWatermarks: workspaceLastPushedWatermarksRef.current
374
+ });
375
+ }, [tasks, archivedTasks, taxonomies, workspaceSyncDocuments]);
376
+ // Fire-and-forget: persist browser-side sync event to the local SQLite event log.
377
+ const reportSyncEvent = useCallback((workspaceId, event) => {
378
+ if (runtimeMode !== 'local')
379
+ return;
380
+ fetch('/api/taskforce/sync/events', {
381
+ method: 'POST',
382
+ credentials: 'include',
383
+ headers: { 'Content-Type': 'application/json' },
384
+ body: JSON.stringify({ ...event, workspaceId })
385
+ }).catch((error) => {
386
+ logSyncDebug('sync_event_post_failed', {
387
+ workspaceId,
388
+ eventType: event.eventType,
389
+ status: event.status,
390
+ error: String(error?.message || error || '')
391
+ });
392
+ });
393
+ }, [runtimeMode]);
394
+ const buildWorkspaceSyncSignature = useCallback(() => {
395
+ return buildWorkspaceSyncSignatureModel({
396
+ workspaceId: currentWorkspaceId,
397
+ tasks: tasks || [],
398
+ archivedTasks: archivedTasks || [],
399
+ pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
400
+ taxonomies: taxonomies || [],
401
+ documents: workspaceSyncDocuments || []
402
+ });
403
+ }, [tasks, archivedTasks, currentWorkspaceId, taxonomies, workspaceSyncDocuments]);
404
+ const refreshWorkspaceSyncDocumentsSnapshot = useCallback(async () => {
405
+ if (runtimeMode !== 'local')
406
+ return;
407
+ const workspaceKey = String(currentWorkspaceId || '').trim();
408
+ if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
409
+ return;
410
+ try {
411
+ const res = await fetch(`/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceKey)}`, {
412
+ method: 'GET',
413
+ credentials: 'include'
414
+ });
415
+ if (!res.ok)
416
+ return;
417
+ const data = await res.json().catch(() => ({}));
418
+ const docs = Array.isArray(data?.documents)
419
+ ? data.documents
420
+ .map((entry) => ({
421
+ path: String(entry?.path || '').trim(),
422
+ updatedAt: String(entry?.updatedAt || '').trim(),
423
+ content: typeof entry?.content === 'string' ? entry.content : ''
424
+ }))
425
+ .filter((entry) => entry.path.length > 0)
426
+ : [];
427
+ const fingerprint = typeof data?.fingerprint === 'string'
428
+ ? data.fingerprint
429
+ : JSON.stringify(docs.map((entry) => `${entry.path}:${entry.updatedAt}:${entry.content.length}`).sort());
430
+ if (fingerprint === workspaceSyncDocumentsFingerprintRef.current)
431
+ return;
432
+ workspaceSyncDocumentsFingerprintRef.current = fingerprint;
433
+ setWorkspaceSyncDocuments(docs);
434
+ setWorkspaceSyncDocumentsFingerprint(fingerprint);
435
+ }
436
+ catch (error) {
437
+ const detail = String(error?.message || error || '');
438
+ logSyncDebug('documents_snapshot_refresh_failed', {
439
+ workspaceId: workspaceKey,
440
+ error: detail
441
+ });
442
+ reportSyncEvent(workspaceKey, {
443
+ eventType: 'push',
444
+ status: 'error',
445
+ errorMessage: `Workspace documents snapshot refresh failed: ${detail}`
446
+ });
447
+ }
448
+ }, [runtimeMode, currentWorkspaceId, reportSyncEvent]);
449
+ const handleSyncAuthFailure = useCallback(async (source, statusCode) => {
450
+ clearWorkspaceRetry();
451
+ logSyncDebug('sync_auth_failure', { source, status: statusCode, workspaceId: currentWorkspaceId });
452
+ incrementWorkspaceSyncAuthFailure();
453
+ recordWorkspaceSyncFailure(source, statusCode);
454
+ setUserGlobalSyncStatus('error');
455
+ const message = 'Session expired. Sign in again to resume cloud sync.';
456
+ setUserGlobalSyncError(message);
457
+ setWorkspaceLastErrorMessage(message);
458
+ setAuthBlocked(true);
459
+ setIsAuthenticated(false);
460
+ await checkAuthSession();
461
+ }, [clearWorkspaceRetry, currentWorkspaceId, checkAuthSession, recordWorkspaceSyncFailure, incrementWorkspaceSyncAuthFailure, setAuthBlocked, setIsAuthenticated]);
462
+ const ensureWorkspaceSyncReady = useCallback(async () => {
463
+ const workspaceKey = String(currentWorkspaceId || '').trim();
464
+ if (!workspaceKey || workspaceKey.toLowerCase() === 'default') {
465
+ logSyncDebug('sync_ready_blocked_unresolved_workspace', { workspaceKey, runtimeMode });
466
+ setUserGlobalSyncStatus('error');
467
+ const message = 'Workspace sync blocked: local workspace ID is unresolved.';
468
+ setUserGlobalSyncError(message);
469
+ setWorkspaceLastErrorMessage(message);
470
+ setWorkspaceLastErrorAt(new Date().toISOString());
471
+ setWorkspaceSyncReady(false);
472
+ return false;
473
+ }
474
+ if (workspaceSyncReadyRef.current.has(workspaceKey))
475
+ return true;
476
+ const ensured = await ensureCloudWorkspaceReadyForSync();
477
+ if (!ensured.success) {
478
+ logSyncDebug('sync_ready_failed', { workspaceKey, statusCode: ensured.statusCode || null, error: ensured.error || null });
479
+ if (ensured.statusCode === 401) {
480
+ await handleSyncAuthFailure('handshake', 401);
481
+ return false;
482
+ }
483
+ if (ensured.statusCode === 429) {
484
+ recordWorkspaceSyncFailure('handshake', 429);
485
+ setUserGlobalSyncStatus('error');
486
+ const message = ensured.error || 'Sync paused — rate limit reached. Will retry automatically.';
487
+ setUserGlobalSyncError(message);
488
+ setWorkspaceLastErrorMessage(message);
489
+ setWorkspaceLastErrorAt(new Date().toISOString());
490
+ setWorkspaceSyncReady(false);
491
+ scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
492
+ minDelayMs: ensured.retryAfterMs,
493
+ workspaceId: workspaceKey
494
+ });
495
+ return false;
496
+ }
497
+ recordWorkspaceSyncFailure('handshake');
498
+ setUserGlobalSyncStatus('error');
499
+ const message = ensured.error || 'Workspace sync handshake failed.';
500
+ setUserGlobalSyncError(message);
501
+ setWorkspaceLastErrorMessage(message);
502
+ setWorkspaceSyncReady(false);
503
+ return false;
504
+ }
505
+ logSyncDebug('sync_ready_ok', { workspaceKey });
506
+ workspaceSyncReadyRef.current.add(workspaceKey);
507
+ setWorkspaceSyncReady(true);
508
+ return true;
509
+ }, [
510
+ currentWorkspaceId,
511
+ ensureCloudWorkspaceReadyForSync,
512
+ runtimeMode,
513
+ recordWorkspaceSyncFailure,
514
+ handleSyncAuthFailure,
515
+ scheduleWorkspacePullRetry
516
+ ]);
517
+ const pushWorkspaceChangesToCloud = useCallback(async (signature) => {
518
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
519
+ return false;
520
+ if (!isAuthenticated) {
521
+ const authed = await checkAuthSession();
522
+ if (!authed)
523
+ return false;
524
+ }
525
+ const workspaceKey = String(currentWorkspaceId || '').trim();
526
+ if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
527
+ return false;
528
+ if (workspaceSyncSourceOfTruth === 'cloud' && !workspaceCloudHydratedRef.current.has(workspaceKey)) {
529
+ 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
+ return false;
550
+ }
551
+ if (workspacePushInFlightRef.current) {
552
+ workspacePendingSignatureRef.current = signature;
553
+ return false;
554
+ }
555
+ workspacePushInFlightRef.current = true;
556
+ setUserGlobalSyncStatus('syncing');
557
+ setUserGlobalSyncError(null);
558
+ const pushStartedAtMs = Date.now();
559
+ try {
560
+ await refreshWorkspaceSyncDocumentsSnapshot();
561
+ const ready = await ensureWorkspaceSyncReady();
562
+ if (!ready)
563
+ return false;
564
+ const payload = buildWorkspaceSyncPayload();
565
+ logSyncDebug('push_start', {
566
+ workspaceId: currentWorkspaceId,
567
+ changeCount: payload.changes.length,
568
+ deleteCount: payload.deleteTaskIds.size
569
+ });
570
+ setUserGlobalSyncPendingChanges(payload.changes.length);
571
+ const pushResult = await runWorkspacePushSyncService({
572
+ resolveCloudAuthUrl,
573
+ workspaceId: currentWorkspaceId,
574
+ payload,
575
+ ensureCloudWorkspaceReadyForSync
576
+ });
577
+ if (!pushResult.success) {
578
+ if (pushResult.status === 401) {
579
+ await handleSyncAuthFailure('push', 401);
580
+ return false;
581
+ }
582
+ logSyncDebug('push_failed_http', {
583
+ workspaceId: currentWorkspaceId,
584
+ status: pushResult.status || 0,
585
+ error: pushResult.error || null,
586
+ code: pushResult.code || null,
587
+ elapsedMs: Date.now() - pushStartedAtMs,
588
+ requestMs: pushResult.requestMs
589
+ });
590
+ recordWorkspaceSyncFailure('push', pushResult.status || undefined);
591
+ setUserGlobalSyncStatus('error');
592
+ const message = pushResult.error
593
+ ? `Workspace sync push failed (${pushResult.status || 0}): ${pushResult.error}`
594
+ : `Workspace sync push failed (${pushResult.status || 0})`;
595
+ setUserGlobalSyncError(message);
596
+ setWorkspaceLastErrorMessage(message);
597
+ return false;
598
+ }
599
+ workspaceLastPushedSignatureRef.current = signature;
600
+ workspaceLastPushedTaskIdsRef.current = pushResult.currentTaskIds;
601
+ workspaceLastPushedDocumentPathsRef.current = new Set(pushResult.currentDocumentPaths);
602
+ workspaceLastPushedDocumentWatermarksRef.current = new Map(pushResult.currentDocumentWatermarks);
603
+ // Merge newly pushed watermarks into the per-task map so subsequent
604
+ // pushes skip tasks that haven't changed since this push.
605
+ for (const [taskId, watermark] of pushResult.pushedWatermarks) {
606
+ workspaceLastPushedWatermarksRef.current.set(taskId, watermark);
607
+ }
608
+ if (pushResult.deleteTaskIds.size > 0) {
609
+ for (const taskId of pushResult.deleteTaskIds) {
610
+ workspaceDeletedTaskIdsRef.current.delete(taskId);
611
+ workspaceLastPushedWatermarksRef.current.delete(taskId);
612
+ }
613
+ }
614
+ const syncedAt = pushResult.syncedAt || new Date().toISOString();
615
+ logSyncDebug('push_success', {
616
+ workspaceId: currentWorkspaceId,
617
+ syncedAt,
618
+ changeCount: pushResult.changeCount,
619
+ deleteCount: pushResult.deleteCount,
620
+ elapsedMs: Date.now() - pushStartedAtMs,
621
+ requestMs: pushResult.requestMs
622
+ });
623
+ setUserGlobalLastSyncedAt(syncedAt);
624
+ setWorkspaceLastPushAt(syncedAt);
625
+ setUserGlobalSyncPendingChanges(0);
626
+ setUserGlobalSyncStatus('idle');
627
+ setUserGlobalSyncError(null);
628
+ setWorkspaceLastErrorMessage(null);
629
+ setWorkspaceLastErrorAt(null);
630
+ if (Array.isArray(pushResult.emittedEventIds) && pushResult.emittedEventIds.length > 0) {
631
+ for (const rawEventId of pushResult.emittedEventIds) {
632
+ const eventId = String(rawEventId || '').trim();
633
+ if (!eventId)
634
+ continue;
635
+ if (realtimeSuppressedEventIdsRef.current.has(eventId))
636
+ continue;
637
+ realtimeSuppressedEventIdsRef.current.add(eventId);
638
+ realtimeSuppressedEventQueueRef.current.push(eventId);
639
+ }
640
+ while (realtimeSuppressedEventQueueRef.current.length > 2048) {
641
+ const stale = realtimeSuppressedEventQueueRef.current.shift();
642
+ if (stale)
643
+ realtimeSuppressedEventIdsRef.current.delete(stale);
644
+ }
645
+ }
646
+ void persistWorkspaceSyncPatch({
647
+ lastPushAt: syncedAt,
648
+ lastSyncedAt: syncedAt
649
+ });
650
+ return true;
651
+ }
652
+ catch {
653
+ logSyncDebug('push_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pushStartedAtMs });
654
+ recordWorkspaceSyncFailure('push');
655
+ setUserGlobalSyncStatus('error');
656
+ const message = 'Workspace sync push failed.';
657
+ setUserGlobalSyncError(message);
658
+ setWorkspaceLastErrorMessage(message);
659
+ return false;
660
+ }
661
+ finally {
662
+ workspacePushInFlightRef.current = false;
663
+ const pendingSignature = workspacePendingSignatureRef.current;
664
+ if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
665
+ workspacePendingSignatureRef.current = '';
666
+ window.setTimeout(() => {
667
+ void pushWorkspaceChangesToCloud(pendingSignature);
668
+ }, 120);
669
+ }
670
+ }
671
+ }, [
672
+ cloudAuthConfigured,
673
+ runtimeMode,
674
+ isAuthenticated,
675
+ checkAuthSession,
676
+ workspaceCloudSyncEnabled,
677
+ workspaceSyncSourceOfTruth,
678
+ buildWorkspaceSyncPayload,
679
+ ensureWorkspaceSyncReady,
680
+ refreshWorkspaceSyncDocumentsSnapshot,
681
+ ensureCloudWorkspaceReadyForSync,
682
+ handleSyncAuthFailure,
683
+ recordWorkspaceSyncFailure,
684
+ clearWorkspaceRetry,
685
+ isWorkspacePullRetryPending,
686
+ currentWorkspaceId,
687
+ resolveCloudAuthUrl
688
+ ]);
689
+ const pullWorkspaceChangesFromCloud = useCallback(async () => {
690
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled) {
691
+ logSyncDebug('pull_abort', { reason: 'config_disabled', cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled });
692
+ return false;
693
+ }
694
+ if (!isAuthenticated) {
695
+ const authed = await checkAuthSession();
696
+ if (!authed) {
697
+ logSyncDebug('pull_abort', { reason: 'not_authenticated' });
698
+ 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
+ }
710
+ if (workspacePullInFlightRef.current) {
711
+ logSyncDebug('pull_abort', { reason: 'already_in_flight' });
712
+ return false;
713
+ }
714
+ const leaseAcquired = await tryAcquireWorkspacePullLease(workspaceKey);
715
+ if (!leaseAcquired) {
716
+ markWorkspacePullCooldown(workspaceKey, 12_000);
717
+ logSyncDebug('pull_abort', { reason: 'non_leader_tab', workspaceId: workspaceKey });
718
+ return false;
719
+ }
720
+ workspacePullInFlightRef.current = true;
721
+ const bootstrapThisRun = !workspaceCloudHydratedRef.current.has(workspaceKey);
722
+ if (bootstrapThisRun) {
723
+ setWorkspaceBootstrapSyncInProgress(true);
724
+ setWorkspaceBootstrapSyncPages(0);
725
+ setWorkspaceBootstrapSyncAppliedChanges(0);
726
+ }
727
+ setUserGlobalSyncStatus('syncing');
728
+ setUserGlobalSyncError(null);
729
+ const pullStartedAtMs = Date.now();
730
+ try {
731
+ const ready = await ensureWorkspaceSyncReady();
732
+ if (!ready) {
733
+ logSyncDebug('pull_abort', { reason: 'ensure_ready_failed' });
734
+ return false;
735
+ }
736
+ const initialCursor = workspacePullBootstrapDoneRef.current
737
+ ? workspacePullCursorRef.current
738
+ : null;
739
+ logSyncDebug('pull_start', { workspaceId: currentWorkspaceId, cursor: initialCursor });
740
+ const pullResult = await runWorkspacePullSyncService({
741
+ resolveCloudAuthUrl,
742
+ workspaceId: currentWorkspaceId,
743
+ cursor: initialCursor,
744
+ bootstrap: bootstrapThisRun,
745
+ ensureCloudWorkspaceReadyForSync,
746
+ maxPages: 20,
747
+ onPagePulled: bootstrapThisRun ? () => {
748
+ setWorkspaceBootstrapSyncPages((prev) => prev + 1);
749
+ } : undefined,
750
+ onChangesApplied: bootstrapThisRun ? (count) => {
751
+ setWorkspaceBootstrapSyncAppliedChanges((prev) => prev + count);
752
+ } : undefined
753
+ });
754
+ if (!pullResult.success) {
755
+ if (pullResult.kind === 'pull_http') {
756
+ if (pullResult.status === 401) {
757
+ await handleSyncAuthFailure('pull', 401);
758
+ return false;
759
+ }
760
+ logSyncDebug('pull_failed_http', {
761
+ workspaceId: currentWorkspaceId,
762
+ status: pullResult.status || 0,
763
+ cursor: pullResult.cursor,
764
+ elapsedMs: Date.now() - pullStartedAtMs,
765
+ requestMs: pullResult.pullRequestMs,
766
+ applyMs: pullResult.applyMs
767
+ });
768
+ reportSyncEvent(currentWorkspaceId, {
769
+ eventType: 'pull',
770
+ status: 'error',
771
+ statusCode: pullResult.status || 0,
772
+ requestMs: pullResult.pullRequestMs
773
+ });
774
+ recordWorkspaceSyncFailure('pull', pullResult.status || undefined);
775
+ setUserGlobalSyncStatus('error');
776
+ const message = `Workspace sync pull failed (${pullResult.status || 0})`;
777
+ setUserGlobalSyncError(message);
778
+ setWorkspaceLastErrorMessage(message);
779
+ if ((pullResult.status || 0) === 429 || (pullResult.status || 0) >= 500) {
780
+ scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
781
+ minDelayMs: pullResult.retryAfterMs,
782
+ workspaceId: workspaceKey
783
+ });
784
+ }
785
+ else {
786
+ clearWorkspaceRetry();
787
+ }
788
+ return false;
789
+ }
790
+ if (pullResult.kind === 'apply_auth') {
791
+ await handleSyncAuthFailure('apply', 401);
792
+ return false;
793
+ }
794
+ if (pullResult.kind === 'apply_all_candidates') {
795
+ const applyFailures = pullResult.failures || [];
796
+ logSyncDebug('apply_failed_all_candidates', {
797
+ workspaceId: currentWorkspaceId,
798
+ failures: applyFailures
799
+ });
800
+ setUserGlobalSyncStatus('error');
801
+ const detail = applyFailures.length > 0 ? ` [${applyFailures.join(', ')}]` : '';
802
+ recordWorkspaceSyncFailure('apply');
803
+ const message = `Workspace sync apply failed${detail}`;
804
+ setUserGlobalSyncError(message);
805
+ setWorkspaceLastErrorMessage(message);
806
+ if (pullResult.hasTransientApplyFailure) {
807
+ scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
808
+ workspaceId: workspaceKey
809
+ });
810
+ }
811
+ else {
812
+ clearWorkspaceRetry();
813
+ }
814
+ return false;
815
+ }
816
+ if (pullResult.kind === 'apply_payload') {
817
+ logSyncDebug('apply_failed_payload', {
818
+ workspaceId: currentWorkspaceId,
819
+ error: String(pullResult.error || '')
820
+ });
821
+ setUserGlobalSyncStatus('error');
822
+ recordWorkspaceSyncFailure('apply');
823
+ const message = String(pullResult.error || 'Workspace sync apply failed.');
824
+ setUserGlobalSyncError(message);
825
+ setWorkspaceLastErrorMessage(message);
826
+ return false;
827
+ }
828
+ logSyncDebug('pull_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pullStartedAtMs });
829
+ recordWorkspaceSyncFailure('pull');
830
+ setUserGlobalSyncStatus('error');
831
+ const message = 'Workspace sync pull failed.';
832
+ setUserGlobalSyncError(message);
833
+ setWorkspaceLastErrorMessage(message);
834
+ scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
835
+ workspaceId: workspaceKey
836
+ });
837
+ return false;
838
+ }
839
+ if (pullResult.pulledDeleteTaskIds.size > 0) {
840
+ setTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
841
+ setArchivedTasks((prev) => prev.filter((task) => !pullResult.pulledDeleteTaskIds.has(String(task.id || ''))));
842
+ }
843
+ const decryptFailures = Array.isArray(pullResult.documentDecryptFailures)
844
+ ? pullResult.documentDecryptFailures.filter((value) => String(value || '').trim().length > 0)
845
+ : [];
846
+ if (decryptFailures.length > 0) {
847
+ const warning = `Some synced documents could not be decrypted (${decryptFailures.length}).`;
848
+ setWorkspaceLastWarningMessage(warning);
849
+ logSyncDebug('pull_document_decrypt_partial_failure', {
850
+ workspaceId: currentWorkspaceId,
851
+ count: decryptFailures.length,
852
+ paths: decryptFailures
853
+ });
854
+ reportSyncEvent(currentWorkspaceId, {
855
+ eventType: 'apply',
856
+ status: 'error',
857
+ errorMessage: `${warning} paths=${decryptFailures.join(', ')}`,
858
+ changeCount: pullResult.appliedChanges
859
+ });
860
+ }
861
+ else {
862
+ setWorkspaceLastWarningMessage(null);
863
+ }
864
+ 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
+ if (pullResult.appliedChanges > 0 && (pullResult.pulledActiveUpserts || pullResult.pulledArchivedUpserts)) {
876
+ // Source of truth for UI is local DB after apply-local writes.
877
+ try {
878
+ const refreshRequests = [];
879
+ if (pullResult.pulledActiveUpserts) {
880
+ refreshRequests.push(fetch('/api/taskforce/tasks', {
881
+ method: 'GET',
882
+ credentials: 'include'
883
+ }));
884
+ }
885
+ if (pullResult.pulledArchivedUpserts) {
886
+ refreshRequests.push(fetch('/api/taskforce/archive', {
887
+ method: 'GET',
888
+ credentials: 'include'
889
+ }));
890
+ }
891
+ const refreshResponses = await Promise.all(refreshRequests);
892
+ for (const response of refreshResponses) {
893
+ if (!response.ok)
894
+ continue;
895
+ const data = await response.json().catch(() => ({}));
896
+ if (Array.isArray(data?.tasks)) {
897
+ setTasks(data.tasks);
898
+ }
899
+ else if (Array.isArray(data?.archived)) {
900
+ setArchivedTasks(data.archived);
901
+ }
902
+ }
903
+ }
904
+ catch {
905
+ logSyncDebug('pull_local_refresh_failed', {
906
+ workspaceId: currentWorkspaceId,
907
+ appliedChanges: pullResult.appliedChanges
908
+ });
909
+ reportSyncEvent(currentWorkspaceId, {
910
+ eventType: 'apply',
911
+ status: 'error',
912
+ errorMessage: 'Local state refresh failed after apply-local.',
913
+ changeCount: pullResult.appliedChanges
914
+ });
915
+ }
916
+ }
917
+ const syncedAt = pullResult.syncedAt || new Date().toISOString();
918
+ logSyncDebug('pull_success', {
919
+ workspaceId: currentWorkspaceId,
920
+ syncedAt,
921
+ appliedChanges: pullResult.appliedChanges,
922
+ pulledDeleteCount: pullResult.pulledDeleteTaskIds.size,
923
+ cursor: workspacePullCursorRef.current,
924
+ elapsedMs: Date.now() - pullStartedAtMs,
925
+ requestMs: pullResult.pullRequestMs,
926
+ applyMs: pullResult.applyMs,
927
+ pages: pullResult.pages
928
+ });
929
+ reportSyncEvent(currentWorkspaceId, {
930
+ eventType: 'pull',
931
+ status: 'success',
932
+ changeCount: pullResult.appliedChanges,
933
+ requestMs: pullResult.pullRequestMs
934
+ });
935
+ setUserGlobalLastSyncedAt(syncedAt);
936
+ setWorkspaceLastPullAt(syncedAt);
937
+ setUserGlobalSyncPendingChanges(0);
938
+ setUserGlobalSyncStatus('idle');
939
+ setUserGlobalSyncError(null);
940
+ setWorkspaceLastErrorMessage(null);
941
+ setWorkspaceLastErrorAt(null);
942
+ clearWorkspaceRetry();
943
+ void persistWorkspaceSyncPatch({
944
+ pullCursor: workspacePullCursorRef.current,
945
+ lastPullAt: syncedAt,
946
+ lastSyncedAt: syncedAt
947
+ });
948
+ // I6: delay onboardingCompleted until first successful pull-and-apply.
949
+ // The setup flow intentionally starts with onboardingCompleted=false so that
950
+ // a broken apply-local path re-surfaces the onboarding prompt rather than
951
+ // leaving the user silently stuck with stale data.
952
+ if (!onboardingCompletedRef.current) {
953
+ onboardingCompletedRef.current = true;
954
+ setWorkspaceSyncOnboardingCompleted(true);
955
+ void persistWorkspaceSyncPatch({ onboardingCompleted: true });
956
+ }
957
+ const pendingSignature = workspacePendingSignatureRef.current;
958
+ if (pendingSignature && pendingSignature !== workspaceLastPushedSignatureRef.current) {
959
+ workspacePendingSignatureRef.current = '';
960
+ window.setTimeout(() => {
961
+ void pushWorkspaceChangesToCloud(pendingSignature);
962
+ }, 120);
963
+ }
964
+ return true;
965
+ }
966
+ catch {
967
+ logSyncDebug('pull_failed_exception', { workspaceId: currentWorkspaceId, elapsedMs: Date.now() - pullStartedAtMs });
968
+ recordWorkspaceSyncFailure('pull');
969
+ setUserGlobalSyncStatus('error');
970
+ const message = 'Workspace sync pull failed.';
971
+ setUserGlobalSyncError(message);
972
+ setWorkspaceLastErrorMessage(message);
973
+ scheduleWorkspacePullRetry(() => pullFromCloudRef.current(), {
974
+ workspaceId: workspaceKey
975
+ });
976
+ return false;
977
+ }
978
+ finally {
979
+ if (bootstrapThisRun) {
980
+ setWorkspaceBootstrapSyncInProgress(false);
981
+ }
982
+ workspacePullInFlightRef.current = false;
983
+ releaseWorkspacePullLease(workspaceKey);
984
+ }
985
+ }, [
986
+ cloudAuthConfigured,
987
+ runtimeMode,
988
+ isAuthenticated,
989
+ checkAuthSession,
990
+ workspaceCloudSyncEnabled,
991
+ resolveCloudAuthUrl,
992
+ currentWorkspaceId,
993
+ isWorkspacePullRetryPending,
994
+ ensureWorkspaceSyncReady,
995
+ handleSyncAuthFailure,
996
+ recordWorkspaceSyncFailure,
997
+ clearWorkspaceRetry,
998
+ scheduleWorkspacePullRetry,
999
+ tryAcquireWorkspacePullLease,
1000
+ releaseWorkspacePullLease,
1001
+ markWorkspacePullCooldown,
1002
+ reportSyncEvent,
1003
+ persistWorkspaceSyncPatch,
1004
+ buildWorkspaceSyncSignature
1005
+ ]);
1006
+ // Keep the stable pull ref in sync so ensureWorkspaceSyncReady can schedule
1007
+ // a retry callback without creating a forward-reference dependency cycle.
1008
+ useEffect(() => {
1009
+ pullFromCloudRef.current = pullWorkspaceChangesFromCloud;
1010
+ }, [pullWorkspaceChangesFromCloud]);
1011
+ // Mirror onboardingCompleted state to a ref so the pull callback can read it
1012
+ // without stale-closure risk.
1013
+ useEffect(() => {
1014
+ onboardingCompletedRef.current = workspaceSyncOnboardingCompleted;
1015
+ }, [workspaceSyncOnboardingCompleted]);
1016
+ const handleRealtimeSignal = useCallback((signal) => {
1017
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1018
+ return;
1019
+ 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
+ });
1026
+ return;
1027
+ }
1028
+ if (isWorkspacePullRetryPending() || workspacePullInFlightRef.current)
1029
+ return;
1030
+ if (realtimePullDebounceRef.current !== null) {
1031
+ window.clearTimeout(realtimePullDebounceRef.current);
1032
+ }
1033
+ realtimePullDebounceRef.current = window.setTimeout(() => {
1034
+ realtimePullDebounceRef.current = null;
1035
+ void pullFromCloudRef.current();
1036
+ }, 180);
1037
+ }, [
1038
+ cloudAuthConfigured,
1039
+ runtimeMode,
1040
+ workspaceCloudSyncEnabled,
1041
+ isWorkspacePullRetryPending,
1042
+ workspacePullInFlightRef,
1043
+ currentWorkspaceId
1044
+ ]);
1045
+ const realtimeSocketUrl = resolveWebSocketUrl('/taskforce-ws');
1046
+ const realtimeEnabled = Boolean(realtimeSyncEnabled
1047
+ && cloudAuthConfigured
1048
+ && runtimeMode === 'local'
1049
+ && workspaceCloudSyncEnabled
1050
+ && isAuthenticated);
1051
+ const realtimeSync = useRealtimeSync({
1052
+ enabled: realtimeEnabled,
1053
+ workspaceId: currentWorkspaceId,
1054
+ websocketUrl: realtimeSocketUrl,
1055
+ onSignal: handleRealtimeSignal,
1056
+ onTelemetry: setRealtimeTelemetry
1057
+ });
1058
+ useEffect(() => {
1059
+ setRealtimeConnectionState(realtimeSync.connectionState);
1060
+ }, [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
+ useEffect(() => {
1083
+ return () => {
1084
+ if (realtimePullDebounceRef.current !== null) {
1085
+ window.clearTimeout(realtimePullDebounceRef.current);
1086
+ realtimePullDebounceRef.current = null;
1087
+ }
1088
+ };
1089
+ }, []);
1090
+ const fetchLocalWorkspaceSnapshot = useCallback(async () => {
1091
+ const [localTasksRes, localArchiveRes] = await Promise.all([
1092
+ fetch('/api/taskforce/tasks', {
1093
+ method: 'GET',
1094
+ credentials: 'include'
1095
+ }),
1096
+ fetch('/api/taskforce/archive', {
1097
+ method: 'GET',
1098
+ credentials: 'include'
1099
+ })
1100
+ ]);
1101
+ if (!localTasksRes.ok || !localArchiveRes.ok) {
1102
+ return {
1103
+ tasks: [],
1104
+ archived: [],
1105
+ error: 'Failed to read local workspace snapshot before sync bootstrap.'
1106
+ };
1107
+ }
1108
+ const localTasksPayload = await localTasksRes.json().catch(() => ({}));
1109
+ const localArchivePayload = await localArchiveRes.json().catch(() => ({}));
1110
+ return {
1111
+ tasks: Array.isArray(localTasksPayload?.tasks) ? localTasksPayload.tasks : [],
1112
+ archived: Array.isArray(localArchivePayload?.archived) ? localArchivePayload.archived : []
1113
+ };
1114
+ }, []);
1115
+ const saveWorkspaceCloudSyncSettings = useCallback(async (input) => {
1116
+ return saveWorkspaceCloudSyncSettingsBase(input, {
1117
+ cloudAuthConfigured,
1118
+ isAuthenticated,
1119
+ resolveCloudAuthUrl,
1120
+ ensureCloudWorkspaceReadyForSync,
1121
+ fetchLocalWorkspaceSnapshot
1122
+ });
1123
+ }, [
1124
+ cloudAuthConfigured,
1125
+ isAuthenticated,
1126
+ resolveCloudAuthUrl,
1127
+ ensureCloudWorkspaceReadyForSync,
1128
+ fetchLocalWorkspaceSnapshot,
1129
+ saveWorkspaceCloudSyncSettingsBase
1130
+ ]);
1131
+ // -----------------------------------------------------------------------
1132
+ // Retry / reset functions
1133
+ // -----------------------------------------------------------------------
1134
+ const retryUserGlobalSettingsSync = useCallback(async () => {
1135
+ await syncUserGlobalSettings({ preferCloudOnFirstSync: false });
1136
+ }, [syncUserGlobalSettings]);
1137
+ const retryWorkspaceCloudSync = useCallback(async () => {
1138
+ clearWorkspaceRetry();
1139
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1140
+ return;
1141
+ if (!isAuthenticated) {
1142
+ const authed = await checkAuthSession();
1143
+ if (!authed)
1144
+ return;
1145
+ }
1146
+ const signature = buildWorkspaceSyncSignature();
1147
+ if (signature && signature !== workspaceLastPushedSignatureRef.current) {
1148
+ void pushWorkspaceChangesToCloud(signature);
1149
+ }
1150
+ await pullWorkspaceChangesFromCloud();
1151
+ }, [
1152
+ clearWorkspaceRetry,
1153
+ cloudAuthConfigured,
1154
+ runtimeMode,
1155
+ isAuthenticated,
1156
+ checkAuthSession,
1157
+ workspaceCloudSyncEnabled,
1158
+ buildWorkspaceSyncSignature,
1159
+ pushWorkspaceChangesToCloud,
1160
+ pullWorkspaceChangesFromCloud
1161
+ ]);
1162
+ const resetWorkspaceSyncCursorAndPull = useCallback(async () => {
1163
+ clearWorkspaceRetry();
1164
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1165
+ return;
1166
+ if (!isAuthenticated) {
1167
+ const authed = await checkAuthSession();
1168
+ if (!authed)
1169
+ return;
1170
+ }
1171
+ workspacePullCursorRef.current = null;
1172
+ workspacePullBootstrapDoneRef.current = false;
1173
+ const workspaceKey = String(currentWorkspaceId || '').trim();
1174
+ if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
1175
+ return;
1176
+ workspaceCloudHydratedRef.current.delete(workspaceKey);
1177
+ setWorkspaceCloudHydrated(false);
1178
+ // Reset watermarks so the next push re-sends the full snapshot.
1179
+ workspaceLastPushedWatermarksRef.current = new Map();
1180
+ logSyncDebug('pull_cursor_reset', { workspaceId: currentWorkspaceId });
1181
+ void persistWorkspaceSyncPatch({ pullCursor: null });
1182
+ await pullWorkspaceChangesFromCloud();
1183
+ }, [
1184
+ clearWorkspaceRetry,
1185
+ cloudAuthConfigured,
1186
+ runtimeMode,
1187
+ isAuthenticated,
1188
+ checkAuthSession,
1189
+ workspaceCloudSyncEnabled,
1190
+ currentWorkspaceId,
1191
+ pullWorkspaceChangesFromCloud
1192
+ ]);
1193
+ // -----------------------------------------------------------------------
1194
+ // Sync effects
1195
+ // -----------------------------------------------------------------------
1196
+ // Documents refresh interval
1197
+ useEffect(() => {
1198
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1199
+ return;
1200
+ void refreshWorkspaceSyncDocumentsSnapshot();
1201
+ const docsIntervalId = window.setInterval(() => {
1202
+ void refreshWorkspaceSyncDocumentsSnapshot();
1203
+ }, 12000);
1204
+ return () => window.clearInterval(docsIntervalId);
1205
+ }, [
1206
+ cloudAuthConfigured,
1207
+ runtimeMode,
1208
+ workspaceCloudSyncEnabled,
1209
+ refreshWorkspaceSyncDocumentsSnapshot
1210
+ ]);
1211
+ // Push on signature change
1212
+ useEffect(() => {
1213
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1214
+ return;
1215
+ const signature = buildWorkspaceSyncSignature();
1216
+ if (!signature || signature === workspaceLastPushedSignatureRef.current)
1217
+ return;
1218
+ if (workspacePushInFlightRef.current) {
1219
+ workspacePendingSignatureRef.current = signature;
1220
+ return;
1221
+ }
1222
+ const timeoutId = window.setTimeout(() => {
1223
+ void pushWorkspaceChangesToCloud(signature);
1224
+ }, 1500);
1225
+ return () => window.clearTimeout(timeoutId);
1226
+ }, [
1227
+ cloudAuthConfigured,
1228
+ runtimeMode,
1229
+ workspaceCloudSyncEnabled,
1230
+ buildWorkspaceSyncSignature,
1231
+ workspaceSyncDocumentsFingerprint,
1232
+ pushWorkspaceChangesToCloud
1233
+ ]);
1234
+ // Clear sync state when disabled
1235
+ useEffect(() => {
1236
+ if (cloudAuthConfigured && runtimeMode === 'local' && workspaceCloudSyncEnabled)
1237
+ return;
1238
+ clearWorkspaceRetry();
1239
+ setWorkspaceSyncReady(false);
1240
+ setWorkspaceCloudHydrated(false);
1241
+ }, [
1242
+ cloudAuthConfigured,
1243
+ runtimeMode,
1244
+ workspaceCloudSyncEnabled,
1245
+ clearWorkspaceRetry
1246
+ ]);
1247
+ // Pull interval
1248
+ useEffect(() => {
1249
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1250
+ return;
1251
+ if (isWorkspacePullRetryPending())
1252
+ return;
1253
+ void pullWorkspaceChangesFromCloud();
1254
+ const intervalId = window.setInterval(() => {
1255
+ void pullWorkspaceChangesFromCloud();
1256
+ }, 8000);
1257
+ return () => window.clearInterval(intervalId);
1258
+ }, [
1259
+ cloudAuthConfigured,
1260
+ runtimeMode,
1261
+ workspaceCloudSyncEnabled,
1262
+ isWorkspacePullRetryPending,
1263
+ pullWorkspaceChangesFromCloud
1264
+ ]);
1265
+ // Backfill: pull when local is empty
1266
+ useEffect(() => {
1267
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
1268
+ return;
1269
+ if (tasks.length > 0 || archivedTasks.length > 0)
1270
+ return;
1271
+ const workspaceKey = String(currentWorkspaceId || '').trim();
1272
+ if (!workspaceKey || workspaceKey.toLowerCase() === 'default')
1273
+ return;
1274
+ if (workspaceBackfillForcedRef.current.has(workspaceKey))
1275
+ return;
1276
+ workspaceBackfillForcedRef.current.add(workspaceKey);
1277
+ workspacePullCursorRef.current = null;
1278
+ workspacePullBootstrapDoneRef.current = false;
1279
+ void pullWorkspaceChangesFromCloud();
1280
+ }, [
1281
+ cloudAuthConfigured,
1282
+ runtimeMode,
1283
+ workspaceCloudSyncEnabled,
1284
+ currentWorkspaceId,
1285
+ tasks.length,
1286
+ archivedTasks.length,
1287
+ pullWorkspaceChangesFromCloud
1288
+ ]);
1289
+ // User global settings: initial sync on auth
1290
+ useEffect(() => {
1291
+ if (runtimeMode !== 'local' || !isAuthenticated)
1292
+ return;
1293
+ if (!authUserId || authUserId === 'anonymous')
1294
+ return;
1295
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1296
+ }, [runtimeMode, isAuthenticated, authUserId, syncUserGlobalSettings]);
1297
+ // User global settings: push on settings change
1298
+ useEffect(() => {
1299
+ if (runtimeMode !== 'local' || !isAuthenticated)
1300
+ return;
1301
+ if (!authUserId || authUserId === 'anonymous')
1302
+ return;
1303
+ if (!initialUserSettingsSyncCompletedRef.current.has(authUserId))
1304
+ return;
1305
+ if (applyingRemoteUserSettingsRef.current)
1306
+ return;
1307
+ const payload = buildUserGlobalSyncPayload();
1308
+ const signature = JSON.stringify(payload);
1309
+ if (!lastUserGlobalSyncSignatureRef.current) {
1310
+ lastUserGlobalSyncSignatureRef.current = signature;
1311
+ return;
1312
+ }
1313
+ if (signature === lastUserGlobalSyncSignatureRef.current)
1314
+ return;
1315
+ lastUserGlobalSyncSignatureRef.current = signature;
1316
+ setUserGlobalSyncPendingChanges(1);
1317
+ setLocalUserSyncUpdatedAt(authUserId, new Date().toISOString());
1318
+ const timeoutId = window.setTimeout(() => {
1319
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: false });
1320
+ }, 350);
1321
+ return () => window.clearTimeout(timeoutId);
1322
+ }, [
1323
+ runtimeMode,
1324
+ isAuthenticated,
1325
+ authUserId,
1326
+ buildUserGlobalSyncPayload,
1327
+ setLocalUserSyncUpdatedAt,
1328
+ syncUserGlobalSettings
1329
+ ]);
1330
+ // -----------------------------------------------------------------------
1331
+ // Return
1332
+ // -----------------------------------------------------------------------
1333
+ return {
1334
+ // State
1335
+ userGlobalSyncStatus,
1336
+ setUserGlobalSyncStatus,
1337
+ userGlobalLastSyncedAt,
1338
+ setUserGlobalLastSyncedAt,
1339
+ userGlobalSyncPendingChanges,
1340
+ setUserGlobalSyncPendingChanges,
1341
+ userGlobalSyncError,
1342
+ setUserGlobalSyncError,
1343
+ workspaceLastPullAt,
1344
+ setWorkspaceLastPullAt,
1345
+ workspaceLastPushAt,
1346
+ setWorkspaceLastPushAt,
1347
+ workspaceLastErrorAt,
1348
+ setWorkspaceLastErrorAt,
1349
+ workspaceLastErrorMessage,
1350
+ setWorkspaceLastErrorMessage,
1351
+ workspaceLastWarningMessage,
1352
+ setWorkspaceLastWarningMessage,
1353
+ workspaceCloudSyncEnabled,
1354
+ realtimeConnectionState,
1355
+ realtimeTelemetry,
1356
+ setWorkspaceCloudSyncEnabled,
1357
+ workspaceSyncSourceOfTruth,
1358
+ setWorkspaceSyncSourceOfTruth,
1359
+ workspaceSyncOnboardingCompleted,
1360
+ setWorkspaceSyncOnboardingCompleted,
1361
+ workspaceSyncReady,
1362
+ setWorkspaceSyncReady,
1363
+ workspaceCloudHydrated,
1364
+ setWorkspaceCloudHydrated,
1365
+ workspaceSyncDocuments,
1366
+ setWorkspaceSyncDocuments,
1367
+ workspaceSyncDocumentsFingerprint,
1368
+ setWorkspaceSyncDocumentsFingerprint,
1369
+ // Refs
1370
+ syncInFlightRef,
1371
+ // From useWorkspaceSyncController
1372
+ workspaceRetryAt,
1373
+ isWorkspacePullRetryPending,
1374
+ workspaceBootstrapSyncInProgress,
1375
+ setWorkspaceBootstrapSyncInProgress,
1376
+ workspaceBootstrapSyncPages,
1377
+ setWorkspaceBootstrapSyncPages,
1378
+ workspaceBootstrapSyncAppliedChanges,
1379
+ setWorkspaceBootstrapSyncAppliedChanges,
1380
+ workspaceSyncFailureCount,
1381
+ workspaceSyncRetryScheduledCount,
1382
+ workspaceSyncAuthFailureCount,
1383
+ workspaceSyncLastFailureSource,
1384
+ workspaceSyncLastFailureStatusCode,
1385
+ workspacePushInFlightRef,
1386
+ workspacePullInFlightRef,
1387
+ workspaceLastPushedSignatureRef,
1388
+ workspacePendingSignatureRef,
1389
+ workspaceLastPushedTaskIdsRef,
1390
+ workspaceDeletedTaskIdsRef,
1391
+ workspacePullCursorRef,
1392
+ workspacePullBootstrapDoneRef,
1393
+ workspaceBackfillForcedRef,
1394
+ workspaceSyncReadyRef,
1395
+ workspaceCloudHydratedRef,
1396
+ clearWorkspaceRetry,
1397
+ scheduleWorkspacePullRetry,
1398
+ isWorkspacePullCooldownActive,
1399
+ markWorkspacePullCooldown,
1400
+ tryAcquireWorkspacePullLease,
1401
+ releaseWorkspacePullLease,
1402
+ recordWorkspaceSyncFailure,
1403
+ incrementWorkspaceSyncAuthFailure,
1404
+ persistWorkspaceSyncPatch,
1405
+ loadWorkspaceSyncState,
1406
+ applyWorkspaceSyncStateSnapshot,
1407
+ // Functions
1408
+ syncUserGlobalSettings,
1409
+ getLocalUserSyncUpdatedAt,
1410
+ setLocalUserSyncUpdatedAt,
1411
+ buildUserGlobalSyncPayload,
1412
+ applyRemoteUserGlobalSyncPayload,
1413
+ ensureCloudWorkspaceReadyForSync,
1414
+ buildWorkspaceSyncPayload,
1415
+ buildWorkspaceSyncSignature,
1416
+ refreshWorkspaceSyncDocumentsSnapshot,
1417
+ handleSyncAuthFailure,
1418
+ ensureWorkspaceSyncReady,
1419
+ pushWorkspaceChangesToCloud,
1420
+ pullWorkspaceChangesFromCloud,
1421
+ fetchLocalWorkspaceSnapshot,
1422
+ saveWorkspaceCloudSyncSettings,
1423
+ retryUserGlobalSettingsSync,
1424
+ retryWorkspaceCloudSync,
1425
+ resetWorkspaceSyncCursorAndPull
1426
+ };
1427
+ }