@taskforcehq/taskforce 0.3.324 → 0.3.327

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 (1361) hide show
  1. package/LICENSE.md +68 -0
  2. package/README.md +19 -0
  3. package/dist/Taskforce.module.css +2191 -540
  4. package/dist/TaskforceCore.d.ts +1 -0
  5. package/dist/TaskforceCore.js +65 -22
  6. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +1 -0
  7. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +3 -2
  8. package/dist/annotatedAttachments/commandService.d.ts +68 -0
  9. package/dist/annotatedAttachments/commandService.js +348 -0
  10. package/dist/annotatedAttachments/service.d.ts +2 -0
  11. package/dist/annotatedAttachments/service.js +13 -12
  12. package/dist/cli.js +158 -9
  13. package/dist/cliOpenPort.d.ts +1 -0
  14. package/dist/cliOpenPort.js +4 -0
  15. package/dist/compat/taskforceAgentDefinitionCompat.d.ts +12 -0
  16. package/dist/compat/taskforceAgentDefinitionCompat.js +25 -0
  17. package/dist/compat/workspaceSyncCompat.js +6 -2
  18. package/dist/compat/workspaceSyncV3CombinedCoverageCompat.d.ts +6 -0
  19. package/dist/compat/workspaceSyncV3CombinedCoverageCompat.js +22 -0
  20. package/dist/compat/workspaceSyncV3OperationCompat.d.ts +7 -0
  21. package/dist/compat/workspaceSyncV3OperationCompat.js +30 -0
  22. package/dist/components/activity/EntityActivityTimeline.d.ts +10 -2
  23. package/dist/components/activity/EntityActivityTimeline.js +73 -35
  24. package/dist/components/context/AttachmentPreviewHost.d.ts +6 -0
  25. package/dist/components/context/AttachmentPreviewHost.js +222 -0
  26. package/dist/components/context/ContextAttachmentManager.d.ts +4 -0
  27. package/dist/components/context/ContextAttachmentManager.js +418 -208
  28. package/dist/components/features/AiProfilesModule.d.ts +24 -0
  29. package/dist/components/features/AiProfilesModule.js +744 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +4 -1
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.js +351 -79
  32. package/dist/components/features/DocumentGenerateModal.js +23 -5
  33. package/dist/components/features/DocumentIndex.d.ts +19 -1
  34. package/dist/components/features/DocumentIndex.js +104 -15
  35. package/dist/components/features/DocumentViewer.d.ts +9 -1
  36. package/dist/components/features/DocumentViewer.js +721 -100
  37. package/dist/components/features/DocumentWorkspace.d.ts +37 -2
  38. package/dist/components/features/DocumentWorkspace.js +21 -6
  39. package/dist/components/features/TaskSettings.d.ts +6 -27
  40. package/dist/components/features/TaskSettings.js +257 -151
  41. package/dist/components/features/TaskforceAgentsModule.d.ts +36 -0
  42. package/dist/components/features/TaskforceAgentsModule.js +1782 -0
  43. package/dist/components/features/agentBehavior/AgentBehaviorManagerShared.d.ts +94 -0
  44. package/dist/components/features/agentBehavior/AgentBehaviorManagerShared.js +70 -0
  45. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +65 -0
  46. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +115 -0
  47. package/dist/components/features/agentRoles/RoleManagerPanel.d.ts +4 -0
  48. package/dist/components/features/agentRoles/RoleManagerPanel.js +382 -0
  49. package/dist/components/features/agentSkills/SkillManagerPanel.d.ts +5 -0
  50. package/dist/components/features/agentSkills/SkillManagerPanel.js +355 -0
  51. package/dist/components/features/documentWorkspaceModel.d.ts +16 -4
  52. package/dist/components/features/documentWorkspaceModel.js +30 -5
  53. package/dist/components/features/planning/PlanningArchiveConfirmation.d.ts +9 -0
  54. package/dist/components/features/planning/PlanningArchiveConfirmation.js +19 -0
  55. package/dist/components/features/planning/PlanningCardIdentity.d.ts +12 -0
  56. package/dist/components/features/planning/PlanningCardIdentity.js +44 -0
  57. package/dist/components/features/planning/PlanningEntityDetails.d.ts +36 -0
  58. package/dist/components/features/planning/PlanningEntityDetails.js +131 -0
  59. package/dist/components/features/planning/PlanningEntitySummary.d.ts +44 -0
  60. package/dist/components/features/planning/PlanningEntitySummary.js +97 -0
  61. package/dist/components/features/planning/PlanningLinkPicker.d.ts +9 -0
  62. package/dist/components/features/planning/PlanningLinkPicker.js +26 -0
  63. package/dist/components/features/planning/PlanningModule.d.ts +6 -0
  64. package/dist/components/features/planning/PlanningModule.js +571 -0
  65. package/dist/components/features/planning/PlanningOwnerDropdown.d.ts +27 -0
  66. package/dist/components/features/planning/PlanningOwnerDropdown.js +80 -0
  67. package/dist/components/features/planning/PlanningTaskArrangeControl.d.ts +5 -0
  68. package/dist/components/features/planning/PlanningTaskArrangeControl.js +11 -0
  69. package/dist/components/features/planning/PlanningTaskCard.d.ts +20 -0
  70. package/dist/components/features/planning/PlanningTaskCard.js +26 -0
  71. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +10 -0
  72. package/dist/components/features/planning/PlanningTaskOrderList.js +52 -0
  73. package/dist/components/features/planning/PlanningUnlinkConfirmation.d.ts +15 -0
  74. package/dist/components/features/planning/PlanningUnlinkConfirmation.js +7 -0
  75. package/dist/components/features/planning/planningCreateIntent.d.ts +10 -0
  76. package/dist/components/features/planning/planningCreateIntent.js +16 -0
  77. package/dist/components/features/planning/planningNavigator.d.ts +33 -0
  78. package/dist/components/features/planning/planningNavigator.js +96 -0
  79. package/dist/components/features/planning/planningTaskArrangement.d.ts +18 -0
  80. package/dist/components/features/planning/planningTaskArrangement.js +106 -0
  81. package/dist/components/features/planning/planningWorkspaceModel.d.ts +121 -0
  82. package/dist/components/features/planning/planningWorkspaceModel.js +5 -0
  83. package/dist/components/features/settings/CategoryManager.js +2 -2
  84. package/dist/components/features/settings/TaxonomyEditor.js +1 -1
  85. package/dist/components/features/settings/TaxonomyManager.js +1 -1
  86. package/dist/components/features/settings/TypeManager.js +1 -1
  87. package/dist/components/features/taskforceAgentAssignments/AgentBehaviorAssignments.d.ts +10 -0
  88. package/dist/components/features/taskforceAgentAssignments/AgentBehaviorAssignments.js +175 -0
  89. package/dist/components/features/taskforceAgentAssignments/agentBehaviorCatalogApi.d.ts +27 -0
  90. package/dist/components/features/taskforceAgentAssignments/agentBehaviorCatalogApi.js +24 -0
  91. package/dist/components/features/taskforceAgentEditorModel.d.ts +109 -0
  92. package/dist/components/features/taskforceAgentEditorModel.js +145 -0
  93. package/dist/components/features/useDocumentWorkspaceController.d.ts +20 -1
  94. package/dist/components/features/useDocumentWorkspaceController.js +106 -21
  95. package/dist/components/features/workflowManager/WorkflowManagerModule.d.ts +11 -0
  96. package/dist/components/features/workflowManager/WorkflowManagerModule.js +313 -0
  97. package/dist/components/features/workflowManager/WorkflowPublishDialog.d.ts +11 -0
  98. package/dist/components/features/workflowManager/WorkflowPublishDialog.js +8 -0
  99. package/dist/components/features/workflowManager/types.d.ts +74 -0
  100. package/dist/components/features/workflowManager/types.js +1 -0
  101. package/dist/components/features/workflowManager/useWorkflowManager.d.ts +30 -0
  102. package/dist/components/features/workflowManager/useWorkflowManager.js +213 -0
  103. package/dist/components/features/workflowManager/workflowManagerApi.d.ts +52 -0
  104. package/dist/components/features/workflowManager/workflowManagerApi.js +142 -0
  105. package/dist/components/task/TaskActionHeader.d.ts +6 -3
  106. package/dist/components/task/TaskActionHeader.js +46 -53
  107. package/dist/components/task/TaskCard.d.ts +10 -2
  108. package/dist/components/task/TaskCard.js +103 -54
  109. package/dist/components/task/TaskContextUpload.d.ts +1 -0
  110. package/dist/components/task/TaskContextUpload.js +3 -2
  111. package/dist/components/task/TaskForm.d.ts +12 -17
  112. package/dist/components/task/TaskForm.js +544 -199
  113. package/dist/components/task/TaskImageReviews.d.ts +8 -0
  114. package/dist/components/task/TaskImageReviews.js +47 -0
  115. package/dist/components/task/TaskKanban.d.ts +11 -12
  116. package/dist/components/task/TaskKanban.js +70 -18
  117. package/dist/components/task/TaskList.d.ts +9 -8
  118. package/dist/components/task/TaskList.js +46 -32
  119. package/dist/components/task/TaskReferenceBadge.d.ts +2 -1
  120. package/dist/components/task/TaskReferenceBadge.js +3 -2
  121. package/dist/components/task/TaskStatusActions.d.ts +3 -1
  122. package/dist/components/task/TaskStatusActions.js +17 -6
  123. package/dist/components/task/TaskWorkflowAssignmentField.d.ts +18 -0
  124. package/dist/components/task/TaskWorkflowAssignmentField.js +362 -0
  125. package/dist/components/task/TaskWorkstreamPicker.d.ts +8 -0
  126. package/dist/components/task/TaskWorkstreamPicker.js +24 -0
  127. package/dist/components/task/TrashRestoreControls.d.ts +18 -0
  128. package/dist/components/task/TrashRestoreControls.js +71 -0
  129. package/dist/components/ui/AiIdentityRosterCard.d.ts +20 -0
  130. package/dist/components/ui/AiIdentityRosterCard.js +5 -0
  131. package/dist/components/ui/AssetTraySortControl.d.ts +10 -0
  132. package/dist/components/ui/AssetTraySortControl.js +38 -0
  133. package/dist/components/ui/AssigneeIdentity.d.ts +12 -0
  134. package/dist/components/ui/AssigneeIdentity.js +41 -0
  135. package/dist/components/ui/DiscardChangesDialog.d.ts +10 -0
  136. package/dist/components/ui/DiscardChangesDialog.js +7 -0
  137. package/dist/components/ui/DisclosureSectionHeader.d.ts +4 -1
  138. package/dist/components/ui/DisclosureSectionHeader.js +2 -2
  139. package/dist/components/ui/EditableAvatarButton.d.ts +6 -1
  140. package/dist/components/ui/EditableAvatarButton.js +6 -5
  141. package/dist/components/ui/HierarchyActionBadge.d.ts +2 -1
  142. package/dist/components/ui/HierarchyActionBadge.js +2 -2
  143. package/dist/components/ui/Markdown.d.ts +13 -3
  144. package/dist/components/ui/Markdown.fixture.d.ts +1 -0
  145. package/dist/components/ui/Markdown.fixture.js +23 -0
  146. package/dist/components/ui/Markdown.js +158 -68
  147. package/dist/components/ui/Modal.d.ts +3 -1
  148. package/dist/components/ui/Modal.js +85 -19
  149. package/dist/components/ui/MultiSelectFilter.d.ts +9 -2
  150. package/dist/components/ui/MultiSelectFilter.js +26 -5
  151. package/dist/components/ui/ReferenceBadgeButton.js +2 -3
  152. package/dist/components/ui/TaxonomyDropdown.d.ts +10 -1
  153. package/dist/components/ui/TaxonomyDropdown.js +149 -58
  154. package/dist/components/ui/TopNoticeLayer.d.ts +2 -1
  155. package/dist/components/ui/TopNoticeLayer.js +5 -2
  156. package/dist/components/ui/useFallbackImageUrl.d.ts +4 -0
  157. package/dist/components/ui/useFallbackImageUrl.js +20 -0
  158. package/dist/components/views/AppShellHeader.js +2 -2
  159. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  160. package/dist/components/views/StandaloneLayout.js +1684 -620
  161. package/dist/components/views/WidgetView.d.ts +1 -0
  162. package/dist/components/views/WidgetView.js +95 -30
  163. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +4 -2
  164. package/dist/components/views/chrome/WorkspaceChromeSections.js +3 -6
  165. package/dist/components/views/chrome/workspaceChrome.d.ts +5 -8
  166. package/dist/components/views/chrome/workspaceChrome.js +62 -27
  167. package/dist/components/views/panels/FilterToolbar.d.ts +4 -3
  168. package/dist/components/views/panels/FilterToolbar.js +8 -5
  169. package/dist/components/views/panels/FilterToolbarControls.d.ts +11 -2
  170. package/dist/components/views/panels/FilterToolbarControls.js +35 -20
  171. package/dist/components/views/panels/PlanningDrawer.d.ts +62 -51
  172. package/dist/components/views/panels/PlanningDrawer.js +993 -314
  173. package/dist/components/views/planningScope.d.ts +4 -0
  174. package/dist/components/views/planningScope.js +12 -1
  175. package/dist/components/views/standalone/modals/AccountSettingsModal.d.ts +62 -0
  176. package/dist/components/views/standalone/modals/AccountSettingsModal.js +197 -0
  177. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +7 -2
  178. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +56 -9
  179. package/dist/components/views/standalone/modals/HelpModal.d.ts +1 -2
  180. package/dist/components/views/standalone/modals/HelpModal.js +3 -4
  181. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +8 -9
  182. package/dist/components/views/standalone/modals/SyncStatusModal.js +85 -25
  183. package/dist/components/views/standalone/modals/TeamManagementModal.js +1 -1
  184. package/dist/config/applicationBaseUrl.d.ts +2 -0
  185. package/dist/config/applicationBaseUrl.js +23 -0
  186. package/dist/config/envSchema.d.ts +1 -1
  187. package/dist/config/envSchema.js +166 -5
  188. package/dist/core/AdminAuditRetentionService.d.ts +43 -0
  189. package/dist/core/AdminAuditRetentionService.js +126 -0
  190. package/dist/core/AiProfileService.d.ts +34 -0
  191. package/dist/core/AiProfileService.js +516 -252
  192. package/dist/core/AttachmentLinkService.d.ts +10 -2
  193. package/dist/core/AttachmentLinkService.js +44 -12
  194. package/dist/core/AuthIdentityService.d.ts +6 -0
  195. package/dist/core/AuthIdentityService.js +38 -0
  196. package/dist/core/AuthTokenService.d.ts +1 -0
  197. package/dist/core/AuthTokenService.js +13 -0
  198. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -2
  199. package/dist/core/DeletedTaskLifecycleService.js +124 -38
  200. package/dist/core/GlobalSettingsService.d.ts +3 -1
  201. package/dist/core/GlobalSettingsService.js +30 -4
  202. package/dist/core/InitiativeDurableMutationService.d.ts +89 -0
  203. package/dist/core/InitiativeDurableMutationService.js +187 -0
  204. package/dist/core/McpTokenService.d.ts +1 -0
  205. package/dist/core/McpTokenService.js +39 -13
  206. package/dist/core/PlanningCommandPreparationService.d.ts +60 -0
  207. package/dist/core/PlanningCommandPreparationService.js +149 -0
  208. package/dist/core/ScheduleCommandService.d.ts +76 -0
  209. package/dist/core/ScheduleCommandService.js +165 -0
  210. package/dist/core/SyncReconciliationService.d.ts +29 -6
  211. package/dist/core/SyncReconciliationService.js +256 -84
  212. package/dist/core/TaskActivityService.d.ts +22 -2
  213. package/dist/core/TaskActivityService.js +177 -29
  214. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +31 -0
  215. package/dist/core/TaskAttachmentLinksDurableMutationService.js +231 -0
  216. package/dist/core/TaskChecklistCommandService.d.ts +50 -0
  217. package/dist/core/TaskChecklistCommandService.js +111 -0
  218. package/dist/core/TaskDurableCreateMutationService.d.ts +38 -0
  219. package/dist/core/TaskDurableCreateMutationService.js +85 -0
  220. package/dist/core/TaskDurableDeleteMutationService.d.ts +47 -0
  221. package/dist/core/TaskDurableDeleteMutationService.js +54 -0
  222. package/dist/core/TaskDurableLifecycleMutationService.d.ts +38 -0
  223. package/dist/core/TaskDurableLifecycleMutationService.js +42 -0
  224. package/dist/core/TaskDurableRelationshipMutationService.d.ts +58 -0
  225. package/dist/core/TaskDurableRelationshipMutationService.js +66 -0
  226. package/dist/core/TaskDurableRestoreMutationService.d.ts +31 -0
  227. package/dist/core/TaskDurableRestoreMutationService.js +33 -0
  228. package/dist/core/TaskDurableStatusMutationService.d.ts +74 -0
  229. package/dist/core/TaskDurableStatusMutationService.js +247 -0
  230. package/dist/core/TaskDurableSyncMutationService.d.ts +34 -0
  231. package/dist/core/TaskDurableSyncMutationService.js +70 -0
  232. package/dist/core/TaskDurableWorkflowMutationService.d.ts +48 -0
  233. package/dist/core/TaskDurableWorkflowMutationService.js +59 -0
  234. package/dist/core/TaskLifecycleCommandService.d.ts +101 -0
  235. package/dist/core/TaskLifecycleCommandService.js +161 -0
  236. package/dist/core/TaskRootCommandPreparationService.d.ts +35 -0
  237. package/dist/core/TaskRootCommandPreparationService.js +103 -0
  238. package/dist/core/Taskforce.d.ts +479 -59
  239. package/dist/core/Taskforce.js +3671 -841
  240. package/dist/core/WorkflowAssignmentService.d.ts +40 -0
  241. package/dist/core/WorkflowAssignmentService.js +158 -0
  242. package/dist/core/WorkflowExecutionService.d.ts +69 -0
  243. package/dist/core/WorkflowExecutionService.js +342 -0
  244. package/dist/core/WorkflowTaskActivationService.d.ts +32 -0
  245. package/dist/core/WorkflowTaskActivationService.js +63 -0
  246. package/dist/core/WorkflowTemplateService.d.ts +64 -0
  247. package/dist/core/WorkflowTemplateService.js +223 -0
  248. package/dist/core/WorkspaceLifecycleService.d.ts +1 -0
  249. package/dist/core/WorkspaceLifecycleService.js +18 -0
  250. package/dist/core/WorkstreamDurableMutationService.d.ts +90 -0
  251. package/dist/core/WorkstreamDurableMutationService.js +184 -0
  252. package/dist/core/planningReferencePolicy.d.ts +14 -0
  253. package/dist/core/planningReferencePolicy.js +53 -0
  254. package/dist/core/taskReferencePolicy.js +24 -3
  255. package/dist/core/types.d.ts +10 -10
  256. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +22 -2
  257. package/dist/documentReviews/DocumentReviewCommentStore.js +92 -8
  258. package/dist/documentReviews/commandService.d.ts +51 -0
  259. package/dist/documentReviews/commandService.js +63 -0
  260. package/dist/documentReviews/service.d.ts +20 -1
  261. package/dist/documentReviews/service.js +234 -24
  262. package/dist/documentReviews/types.d.ts +18 -1
  263. package/dist/hooks/sync/bootstrap.d.ts +3 -7
  264. package/dist/hooks/sync/bootstrap.js +6 -14
  265. package/dist/hooks/sync/browserSyncAuthority.d.ts +21 -0
  266. package/dist/hooks/sync/browserSyncAuthority.js +70 -0
  267. package/dist/hooks/sync/browserSyncRemoteServices.d.ts +29 -0
  268. package/dist/hooks/sync/browserSyncRemoteServices.js +58 -0
  269. package/dist/hooks/sync/controlPlane.d.ts +1 -37
  270. package/dist/hooks/sync/controlPlane.js +1 -78
  271. package/dist/hooks/sync/lifecyclePolicy.d.ts +1 -45
  272. package/dist/hooks/sync/lifecyclePolicy.js +1 -93
  273. package/dist/hooks/sync/orchestratorShared.d.ts +9 -3
  274. package/dist/hooks/sync/orchestratorShared.js +70 -96
  275. package/dist/hooks/sync/pullLifecycle.d.ts +1 -31
  276. package/dist/hooks/sync/pullLifecycle.js +1 -24
  277. package/dist/hooks/sync/realtime.d.ts +4 -1
  278. package/dist/hooks/sync/realtime.js +103 -3
  279. package/dist/hooks/sync/recovery.d.ts +1 -84
  280. package/dist/hooks/sync/recovery.js +1 -245
  281. package/dist/hooks/sync/transfers.d.ts +8 -129
  282. package/dist/hooks/sync/transfers.js +19 -581
  283. package/dist/hooks/sync/useDataVersionRefresh.d.ts +6 -1
  284. package/dist/hooks/sync/useDataVersionRefresh.js +127 -12
  285. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +14 -0
  286. package/dist/hooks/sync/useDurableLocalOutboxDrain.js +208 -0
  287. package/dist/hooks/sync/useDurableTaskCreateMutation.d.ts +15 -0
  288. package/dist/hooks/sync/useDurableTaskCreateMutation.js +152 -0
  289. package/dist/hooks/sync/useDurableTaskDeleteMutation.d.ts +11 -0
  290. package/dist/hooks/sync/useDurableTaskDeleteMutation.js +21 -0
  291. package/dist/hooks/sync/useDurableTaskLifecycleMutation.d.ts +12 -0
  292. package/dist/hooks/sync/useDurableTaskLifecycleMutation.js +22 -0
  293. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +14 -0
  294. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +24 -0
  295. package/dist/hooks/sync/useDurableTaskRestoreMutation.d.ts +12 -0
  296. package/dist/hooks/sync/useDurableTaskRestoreMutation.js +24 -0
  297. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +18 -0
  298. package/dist/hooks/sync/useDurableTaskStatusMutation.js +23 -0
  299. package/dist/hooks/sync/useLocalSyncCoordinatorSnapshot.d.ts +23 -0
  300. package/dist/hooks/sync/useLocalSyncCoordinatorSnapshot.js +414 -0
  301. package/dist/hooks/sync/useSyncStatusActions.d.ts +2 -1
  302. package/dist/hooks/sync/useSyncStatusActions.js +8 -3
  303. package/dist/hooks/tasks/taskHttpMutation.d.ts +25 -0
  304. package/dist/hooks/tasks/taskHttpMutation.js +134 -0
  305. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +6 -1
  306. package/dist/hooks/tasks/useCurrentTaskRelations.js +32 -6
  307. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +4 -3
  308. package/dist/hooks/tasks/useTaskEditorNavigation.js +9 -13
  309. package/dist/hooks/tasks/useTaskFormActions.d.ts +17 -2
  310. package/dist/hooks/tasks/useTaskFormActions.js +309 -55
  311. package/dist/hooks/tasks/useTaskReferenceContract.d.ts +5 -0
  312. package/dist/hooks/tasks/useTaskReferenceContract.js +17 -0
  313. package/dist/hooks/tasks/useTaskRelationships.d.ts +1 -3
  314. package/dist/hooks/tasks/useTaskRelationships.js +5 -25
  315. package/dist/hooks/tasks/useTrashSelection.d.ts +12 -0
  316. package/dist/hooks/tasks/useTrashSelection.js +49 -0
  317. package/dist/hooks/ui/useInitiativeTemplates.d.ts +24 -0
  318. package/dist/hooks/ui/useInitiativeTemplates.js +22 -0
  319. package/dist/hooks/ui/useSettingsModel.d.ts +5 -29
  320. package/dist/hooks/ui/useSettingsModel.js +31 -15
  321. package/dist/hooks/ui/useSettingsPersistence.d.ts +0 -1
  322. package/dist/hooks/ui/useSettingsPersistence.js +4 -8
  323. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +31 -0
  324. package/dist/hooks/useFilterSort.d.ts +2 -2
  325. package/dist/hooks/useFilterSort.js +19 -15
  326. package/dist/hooks/useRealtimeSync.d.ts +10 -0
  327. package/dist/hooks/useRealtimeSync.js +57 -4
  328. package/dist/hooks/useSyncOrchestrator.d.ts +32 -1
  329. package/dist/hooks/useSyncOrchestrator.js +963 -287
  330. package/dist/hooks/useTaskData.d.ts +11 -2
  331. package/dist/hooks/useTaskData.js +59 -26
  332. package/dist/hooks/useTaskMutations.d.ts +42 -5
  333. package/dist/hooks/useTaskMutations.js +750 -125
  334. package/dist/hooks/useTaskforce.d.ts +114 -118
  335. package/dist/hooks/useTaskforce.js +1106 -396
  336. package/dist/hooks/useUiNotice.d.ts +5 -1
  337. package/dist/hooks/useUiNotice.js +13 -2
  338. package/dist/hooks/useWorkspaceSyncController.d.ts +9 -0
  339. package/dist/hooks/useWorkspaceSyncController.js +159 -17
  340. package/dist/hooks/workspace/usePersistedAppUiState.d.ts +74 -0
  341. package/dist/hooks/workspace/usePersistedAppUiState.js +325 -0
  342. package/dist/hooks/workspace/usePersistedLayoutUiState.d.ts +76 -0
  343. package/dist/hooks/workspace/usePersistedLayoutUiState.js +479 -0
  344. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +2 -7
  345. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +42 -31
  346. package/dist/hooks/workspace/workspaceLocalState.d.ts +42 -6
  347. package/dist/hooks/workspace/workspaceLocalState.js +213 -5
  348. package/dist/hooks/workstreams/workstreamHttpMutation.d.ts +23 -0
  349. package/dist/hooks/workstreams/workstreamHttpMutation.js +134 -0
  350. package/dist/localization/locales/en-US.d.ts +3 -3
  351. package/dist/localization/locales/en-US.js +3 -3
  352. package/dist/localization/locales/es-419.js +3 -3
  353. package/dist/localization/locales/pt-BR.js +3 -3
  354. package/dist/mcp/adminWorkspaceRegistrar.js +2 -40
  355. package/dist/mcp/aiProfileBootstrap.d.ts +3 -0
  356. package/dist/mcp/aiProfileBootstrap.js +4 -0
  357. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  358. package/dist/mcp/canonicalAssetHelpers.js +104 -35
  359. package/dist/mcp/clientIntegrations.d.ts +4 -2
  360. package/dist/mcp/clientIntegrations.js +104 -53
  361. package/dist/mcp/clientRegistry.d.ts +30 -0
  362. package/dist/mcp/clientRegistry.js +134 -0
  363. package/dist/mcp/collaborationRegistrar.js +21 -7
  364. package/dist/mcp/documentAssetRegistrar.js +227 -107
  365. package/dist/mcp/documentContentCommand.d.ts +94 -0
  366. package/dist/mcp/documentContentCommand.js +226 -0
  367. package/dist/mcp/documentHelpers.d.ts +14 -59
  368. package/dist/mcp/documentHelpers.js +52 -61
  369. package/dist/mcp/durableTaskMutationRouter.d.ts +65 -0
  370. package/dist/mcp/durableTaskMutationRouter.js +217 -0
  371. package/dist/mcp/durableTaskRelationshipMutationRouter.d.ts +38 -0
  372. package/dist/mcp/durableTaskRelationshipMutationRouter.js +168 -0
  373. package/dist/mcp/durableTaskRootMutationRouter.d.ts +64 -0
  374. package/dist/mcp/durableTaskRootMutationRouter.js +226 -0
  375. package/dist/mcp/gitWorktreeRoots.d.ts +2 -0
  376. package/dist/mcp/gitWorktreeRoots.js +19 -0
  377. package/dist/mcp/legacyServer.d.ts +1 -0
  378. package/dist/mcp/legacyServer.js +42 -0
  379. package/dist/mcp/localCliHealth.d.ts +56 -0
  380. package/dist/mcp/localCliHealth.js +170 -0
  381. package/dist/mcp/localServiceProxy.d.ts +23 -0
  382. package/dist/mcp/localServiceProxy.js +255 -0
  383. package/dist/mcp/mcpInputValidator.d.ts +17 -0
  384. package/dist/mcp/mcpInputValidator.js +58 -0
  385. package/dist/mcp/planningAttachmentCommandAdapter.d.ts +127 -0
  386. package/dist/mcp/planningAttachmentCommandAdapter.js +188 -0
  387. package/dist/mcp/programScaffoldCommandAdapter.d.ts +6 -0
  388. package/dist/mcp/programScaffoldCommandAdapter.js +39 -0
  389. package/dist/mcp/proxyServer.d.ts +1 -0
  390. package/dist/mcp/proxyServer.js +2 -0
  391. package/dist/mcp/runtime.d.ts +86 -1
  392. package/dist/mcp/runtime.js +4562 -2270
  393. package/dist/mcp/server.js +29 -2
  394. package/dist/mcp/sharedLocalServiceGateD.d.ts +115 -0
  395. package/dist/mcp/sharedLocalServiceGateD.js +344 -0
  396. package/dist/mcp/stdioLifecycle.d.ts +22 -0
  397. package/dist/mcp/stdioLifecycle.js +54 -0
  398. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +190 -0
  399. package/dist/mcp/taskAttachmentCommandAdapter.js +464 -0
  400. package/dist/mcp/taskAttachmentHelpers.d.ts +8 -6
  401. package/dist/mcp/taskAttachmentHelpers.js +74 -2
  402. package/dist/mcp/taskAttachmentTypes.d.ts +26 -5
  403. package/dist/mcp/taskPlanCommand.d.ts +64 -0
  404. package/dist/mcp/taskPlanCommand.js +98 -0
  405. package/dist/mcp/taskPlanningRegistrar.js +253 -101
  406. package/dist/mcp/toolCallCompatibility.d.ts +6 -0
  407. package/dist/mcp/toolCallCompatibility.js +16 -0
  408. package/dist/mcp/toolCatalog.d.ts +3 -1
  409. package/dist/mcp/toolProfiles.d.ts +18 -0
  410. package/dist/mcp/toolProfiles.js +79 -0
  411. package/dist/mcp/toolRegistry.d.ts +102 -609
  412. package/dist/mcp/toolRegistry.js +108 -88
  413. package/dist/mcp/workflowExecutionRegistrar.d.ts +2 -0
  414. package/dist/mcp/workflowExecutionRegistrar.js +51 -0
  415. package/dist/migrations/taskSchemaMigrations.d.ts +35 -0
  416. package/dist/migrations/taskSchemaMigrations.js +1797 -4
  417. package/dist/migrations/taskUpdatedAtSyncDriftMigration.d.ts +20 -0
  418. package/dist/migrations/taskUpdatedAtSyncDriftMigration.js +153 -0
  419. package/dist/migrations/taskforceAgentDefinitionMigration.d.ts +26 -0
  420. package/dist/migrations/taskforceAgentDefinitionMigration.js +116 -0
  421. package/dist/plugins/vite.js +48 -17
  422. package/dist/public/Taskforce_Van.webp +0 -0
  423. package/dist/runtime/appGateDefinitions.d.ts +16 -10
  424. package/dist/runtime/appGateDefinitions.js +16 -10
  425. package/dist/runtime/appGates.d.ts +5 -4
  426. package/dist/runtime/appGates.js +4 -3
  427. package/dist/server/aiProfileAvatarAssets.d.ts +11 -8
  428. package/dist/server/aiProfileAvatarAssets.js +159 -70
  429. package/dist/server/auth.js +15 -1
  430. package/dist/server/cors.d.ts +2 -1
  431. package/dist/server/cors.js +45 -8
  432. package/dist/server/email.d.ts +3 -1
  433. package/dist/server/email.js +46 -8
  434. package/dist/server/index.d.ts +24 -1
  435. package/dist/server/index.js +342 -46
  436. package/dist/server/localDataOwnerPermit.d.ts +18 -0
  437. package/dist/server/localDataOwnerPermit.js +58 -0
  438. package/dist/server/localDatabaseIdentity.d.ts +14 -0
  439. package/dist/server/localDatabaseIdentity.js +69 -0
  440. package/dist/server/localRealtimeFileInvalidation.d.ts +1 -0
  441. package/dist/server/localRealtimeFileInvalidation.js +7 -0
  442. package/dist/server/localServiceLease.d.ts +62 -0
  443. package/dist/server/localServiceLease.js +477 -0
  444. package/dist/server/localServiceOwnershipBootstrap.d.ts +19 -0
  445. package/dist/server/localServiceOwnershipBootstrap.js +84 -0
  446. package/dist/server/localServiceSessions.d.ts +87 -0
  447. package/dist/server/localServiceSessions.js +309 -0
  448. package/dist/server/localServiceWindowsAcl.d.ts +8 -0
  449. package/dist/server/localServiceWindowsAcl.js +60 -0
  450. package/dist/server/localSyncProcessLockBootstrap.d.ts +3 -0
  451. package/dist/server/localSyncProcessLockBootstrap.js +16 -0
  452. package/dist/server/localWorkspaceSyncServerComposition.d.ts +59 -0
  453. package/dist/server/localWorkspaceSyncServerComposition.js +91 -0
  454. package/dist/server/localWorkspaceSyncServerRuntime.d.ts +119 -0
  455. package/dist/server/localWorkspaceSyncServerRuntime.js +1163 -0
  456. package/dist/server/rateLimit.d.ts +1 -0
  457. package/dist/server/rateLimit.js +16 -0
  458. package/dist/server/routes/admin.d.ts +2 -4
  459. package/dist/server/routes/admin.js +199 -337
  460. package/dist/server/routes/agentRoles.d.ts +13 -0
  461. package/dist/server/routes/agentRoles.js +273 -0
  462. package/dist/server/routes/agentSkills.d.ts +16 -0
  463. package/dist/server/routes/agentSkills.js +291 -0
  464. package/dist/server/routes/agents.d.ts +49 -0
  465. package/dist/server/routes/agents.js +1455 -0
  466. package/dist/server/routes/annotatedAttachments.js +46 -9
  467. package/dist/server/routes/assetAccess.d.ts +36 -0
  468. package/dist/server/routes/assetAccess.js +93 -0
  469. package/dist/server/routes/auth.d.ts +4 -3
  470. package/dist/server/routes/auth.js +236 -21
  471. package/dist/server/routes/authSupport.d.ts +3 -1
  472. package/dist/server/routes/authSupport.js +22 -8
  473. package/dist/server/routes/documentReviews.d.ts +2 -0
  474. package/dist/server/routes/documentReviews.js +155 -15
  475. package/dist/server/routes/documents.d.ts +1 -0
  476. package/dist/server/routes/documents.js +335 -141
  477. package/dist/server/routes/durableTaskHttpRouters.d.ts +165 -0
  478. package/dist/server/routes/durableTaskHttpRouters.js +89 -0
  479. package/dist/server/routes/localMcpHealth.d.ts +11 -0
  480. package/dist/server/routes/localMcpHealth.js +29 -0
  481. package/dist/server/routes/localService.d.ts +13 -0
  482. package/dist/server/routes/localService.js +444 -0
  483. package/dist/server/routes/localUpdate.d.ts +11 -0
  484. package/dist/server/routes/localUpdate.js +154 -0
  485. package/dist/server/routes/primitives.d.ts +27 -0
  486. package/dist/server/routes/primitives.js +18 -0
  487. package/dist/server/routes/resources.d.ts +1 -10
  488. package/dist/server/routes/resources.js +1 -153
  489. package/dist/server/routes/shared.d.ts +31 -2
  490. package/dist/server/routes/shared.js +394 -0
  491. package/dist/server/routes/sync.d.ts +2 -0
  492. package/dist/server/routes/sync.js +184 -272
  493. package/dist/server/routes/syncAuxRoutes.d.ts +3 -1
  494. package/dist/server/routes/syncAuxRoutes.js +9 -4
  495. package/dist/server/routes/syncCoordinatorLocalRoutes.d.ts +6 -0
  496. package/dist/server/routes/syncCoordinatorLocalRoutes.js +867 -0
  497. package/dist/server/routes/syncPullApplyRoutes.d.ts +29 -1
  498. package/dist/server/routes/syncPullApplyRoutes.js +520 -17
  499. package/dist/server/routes/syncPushRoutes.d.ts +33 -4
  500. package/dist/server/routes/syncPushRoutes.js +251 -62
  501. package/dist/server/routes/syncRouteTypes.d.ts +1 -1
  502. package/dist/server/routes/syncSnapshotRoutes.d.ts +15 -7
  503. package/dist/server/routes/syncSnapshotRoutes.js +95 -17
  504. package/dist/server/routes/syncV3DiagnosticsRoutes.d.ts +14 -0
  505. package/dist/server/routes/syncV3DiagnosticsRoutes.js +72 -0
  506. package/dist/server/routes/syncV3FeedRoutes.d.ts +20 -0
  507. package/dist/server/routes/syncV3FeedRoutes.js +462 -0
  508. package/dist/server/routes/syncV3LocalCaptureRoutes.d.ts +37 -0
  509. package/dist/server/routes/syncV3LocalCaptureRoutes.js +897 -0
  510. package/dist/server/routes/syncV3LocalDispatchRoutes.d.ts +17 -0
  511. package/dist/server/routes/syncV3LocalDispatchRoutes.js +280 -0
  512. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +17 -0
  513. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +493 -0
  514. package/dist/server/routes/syncV3LocalOwnershipRoutes.d.ts +10 -0
  515. package/dist/server/routes/syncV3LocalOwnershipRoutes.js +51 -0
  516. package/dist/server/routes/syncV3LocalRecoveryRoutes.d.ts +12 -0
  517. package/dist/server/routes/syncV3LocalRecoveryRoutes.js +78 -0
  518. package/dist/server/routes/syncV3OperationRoutes.d.ts +35 -0
  519. package/dist/server/routes/syncV3OperationRoutes.js +1136 -0
  520. package/dist/server/routes/tasks.d.ts +0 -2
  521. package/dist/server/routes/tasks.js +932 -195
  522. package/dist/server/routes/workflows.d.ts +26 -0
  523. package/dist/server/routes/workflows.js +500 -0
  524. package/dist/server/routes.d.ts +1 -6
  525. package/dist/server/routes.js +148 -339
  526. package/dist/server/smtpTransport.d.ts +11 -3
  527. package/dist/server/smtpTransport.js +9 -2
  528. package/dist/server/start.js +42 -10
  529. package/dist/service/localServiceCli.d.ts +33 -0
  530. package/dist/service/localServiceCli.js +365 -0
  531. package/dist/service/localServiceClientLifecycle.d.ts +25 -0
  532. package/dist/service/localServiceClientLifecycle.js +128 -0
  533. package/dist/service/localServiceOpen.d.ts +17 -0
  534. package/dist/service/localServiceOpen.js +27 -0
  535. package/dist/services/agentRoleResolver.d.ts +4 -0
  536. package/dist/services/agentRoleResolver.js +7 -0
  537. package/dist/services/aiProfileAvatarGenerationService.d.ts +41 -0
  538. package/dist/services/aiProfileAvatarGenerationService.js +158 -0
  539. package/dist/services/avatarImageProvider.d.ts +33 -0
  540. package/dist/services/avatarImageProvider.js +140 -0
  541. package/dist/services/bedrockAnthropicMessagesModelGateway.d.ts +24 -0
  542. package/dist/services/bedrockAnthropicMessagesModelGateway.js +95 -0
  543. package/dist/services/bedrockMantleModelGateway.d.ts +43 -0
  544. package/dist/services/bedrockMantleModelGateway.js +81 -0
  545. package/dist/services/bedrockModelGateway.d.ts +26 -0
  546. package/dist/services/bedrockModelGateway.js +94 -0
  547. package/dist/services/modelGateway.d.ts +82 -0
  548. package/dist/services/modelGateway.js +61 -0
  549. package/dist/services/taskforceAgentAvatarImageProvider.d.ts +8 -0
  550. package/dist/services/taskforceAgentAvatarImageProvider.js +24 -0
  551. package/dist/services/taskforceAgentMcpBridge.d.ts +77 -0
  552. package/dist/services/taskforceAgentMcpBridge.js +527 -0
  553. package/dist/shared/agentRoleDefinition.d.ts +34 -0
  554. package/dist/shared/agentRoleDefinition.js +207 -0
  555. package/dist/shared/aiProfileAvatarEligibility.d.ts +10 -0
  556. package/dist/shared/aiProfileAvatarEligibility.js +27 -0
  557. package/dist/shared/aiProfileColors.d.ts +7 -0
  558. package/dist/shared/aiProfileColors.js +33 -0
  559. package/dist/shared/colorPalette.d.ts +3 -0
  560. package/dist/shared/colorPalette.js +20 -0
  561. package/dist/shared/commentProvenance.d.ts +4 -0
  562. package/dist/shared/commentProvenance.js +6 -0
  563. package/dist/shared/deletedTaskRetention.d.ts +2 -0
  564. package/dist/shared/deletedTaskRetention.js +7 -0
  565. package/dist/shared/passwordPolicy.d.ts +2 -1
  566. package/dist/shared/passwordPolicy.js +9 -0
  567. package/dist/shared/taskDeepLinks.d.ts +28 -0
  568. package/dist/shared/taskDeepLinks.js +48 -0
  569. package/dist/shared/taskRestoreDestination.d.ts +2 -0
  570. package/dist/shared/taskRestoreDestination.js +5 -0
  571. package/dist/shared/taskforceAgentDefinition.d.ts +88 -0
  572. package/dist/shared/taskforceAgentDefinition.js +434 -0
  573. package/dist/shared/taskforceAgentExecutionSnapshot.d.ts +50 -0
  574. package/dist/shared/taskforceAgentExecutionSnapshot.js +68 -0
  575. package/dist/shared/taskforceAgentModels.d.ts +56 -0
  576. package/dist/shared/taskforceAgentModels.js +314 -0
  577. package/dist/shared/taskforceAgentRunContext.d.ts +23 -0
  578. package/dist/shared/taskforceAgentRunContext.js +21 -0
  579. package/dist/shared/taskforceAgentRuntimeResolution.d.ts +31 -0
  580. package/dist/shared/taskforceAgentRuntimeResolution.js +72 -0
  581. package/dist/shared/taskforceAgentSkill.d.ts +33 -0
  582. package/dist/shared/taskforceAgentSkill.js +149 -0
  583. package/dist/shared/taxonomyLibrary.js +1 -1
  584. package/dist/storage/agentRoleStore.d.ts +91 -0
  585. package/dist/storage/agentRoleStore.js +385 -0
  586. package/dist/storage/databaseAdapter.d.ts +1 -0
  587. package/dist/storage/databaseAdapter.js +7 -1
  588. package/dist/storage/documentIntegrity.d.ts +12 -3
  589. package/dist/storage/documentIntegrity.js +105 -27
  590. package/dist/storage/objectStorageClient.d.ts +10 -4
  591. package/dist/storage/objectStorageClient.js +73 -43
  592. package/dist/storage/postgresAdapter.d.ts +2 -1
  593. package/dist/storage/postgresAdapter.js +38 -6
  594. package/dist/storage/postgresWorker.js +65 -1
  595. package/dist/storage/sqliteAdapter.d.ts +1 -1
  596. package/dist/storage/sqliteAdapter.js +7 -1
  597. package/dist/storage/sqliteWalHealth.d.ts +71 -0
  598. package/dist/storage/sqliteWalHealth.js +176 -0
  599. package/dist/storage/taskDataPath.d.ts +1 -0
  600. package/dist/storage/taskDataPath.js +20 -0
  601. package/dist/storage/taskforceAgentConversationStore.d.ts +66 -0
  602. package/dist/storage/taskforceAgentConversationStore.js +264 -0
  603. package/dist/storage/taskforceAgentSkillStore.d.ts +93 -0
  604. package/dist/storage/taskforceAgentSkillStore.js +355 -0
  605. package/dist/storage/taskforceAgentStore.d.ts +105 -0
  606. package/dist/storage/taskforceAgentStore.js +549 -0
  607. package/dist/storage/workflowStore.d.ts +223 -0
  608. package/dist/storage/workflowStore.js +1032 -0
  609. package/dist/storage/workspaceAssetStore.d.ts +70 -0
  610. package/dist/storage/workspaceAssetStore.js +418 -25
  611. package/dist/sync/aiProfileAvatarSyncPaths.d.ts +14 -0
  612. package/dist/sync/aiProfileAvatarSyncPaths.js +56 -0
  613. package/dist/sync/cloudSyncApi.d.ts +329 -0
  614. package/dist/sync/cloudSyncApi.js +596 -4
  615. package/dist/sync/collaborationSyncPayload.d.ts +5 -1
  616. package/dist/sync/collaborationSyncPayload.js +213 -1
  617. package/dist/sync/collaborationSyncState.d.ts +23 -3
  618. package/dist/sync/collaborationSyncState.js +92 -3
  619. package/dist/sync/contentSyncPayload.d.ts +9 -0
  620. package/dist/sync/contentSyncPayload.js +29 -0
  621. package/dist/sync/contentSyncState.d.ts +79 -4
  622. package/dist/sync/contentSyncState.js +1113 -313
  623. package/dist/sync/coordinator/localSyncBrowserApplyPermitContext.d.ts +39 -0
  624. package/dist/sync/coordinator/localSyncBrowserApplyPermitContext.js +234 -0
  625. package/dist/sync/coordinator/localSyncBrowserAuthorityCapability.d.ts +7 -0
  626. package/dist/sync/coordinator/localSyncBrowserAuthorityCapability.js +10 -0
  627. package/dist/sync/coordinator/localSyncCommittedApplyPolicy.d.ts +7 -0
  628. package/dist/sync/coordinator/localSyncCommittedApplyPolicy.js +41 -0
  629. package/dist/sync/coordinator/localSyncCoordinatorBrowserClient.d.ts +178 -0
  630. package/dist/sync/coordinator/localSyncCoordinatorBrowserClient.js +131 -0
  631. package/dist/sync/coordinator/localSyncCoordinatorCommandLifecycle.d.ts +25 -0
  632. package/dist/sync/coordinator/localSyncCoordinatorCommandLifecycle.js +200 -0
  633. package/dist/sync/coordinator/localSyncCoordinatorHost.d.ts +84 -0
  634. package/dist/sync/coordinator/localSyncCoordinatorHost.js +435 -0
  635. package/dist/sync/coordinator/localSyncCoordinatorRouteServices.d.ts +33 -0
  636. package/dist/sync/coordinator/localSyncCoordinatorRouteServices.js +1 -0
  637. package/dist/sync/coordinator/localSyncCoordinatorStore.d.ts +122 -0
  638. package/dist/sync/coordinator/localSyncCoordinatorStore.js +1039 -0
  639. package/dist/sync/coordinator/localSyncCoordinatorTypes.d.ts +82 -0
  640. package/dist/sync/coordinator/localSyncCoordinatorTypes.js +9 -0
  641. package/dist/sync/coordinator/localSyncDispatchTracker.d.ts +15 -0
  642. package/dist/sync/coordinator/localSyncDispatchTracker.js +68 -0
  643. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +200 -0
  644. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +1388 -0
  645. package/dist/sync/coordinator/localSyncExecutionPermitTypes.d.ts +52 -0
  646. package/dist/sync/coordinator/localSyncExecutionPermitTypes.js +8 -0
  647. package/dist/sync/coordinator/localSyncOwnershipDrainBarrier.d.ts +33 -0
  648. package/dist/sync/coordinator/localSyncOwnershipDrainBarrier.js +83 -0
  649. package/dist/sync/coordinator/localSyncPermitTransactionHooks.d.ts +9 -0
  650. package/dist/sync/coordinator/localSyncPermitTransactionHooks.js +27 -0
  651. package/dist/sync/coordinator/localSyncProcessLock.d.ts +12 -0
  652. package/dist/sync/coordinator/localSyncProcessLock.js +212 -0
  653. package/dist/sync/coordinator/localSyncV3DispatchRecoveryReconciler.d.ts +17 -0
  654. package/dist/sync/coordinator/localSyncV3DispatchRecoveryReconciler.js +82 -0
  655. package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +134 -0
  656. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +981 -0
  657. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.d.ts +189 -0
  658. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +961 -0
  659. package/dist/sync/coordinator/workspaceSyncCloudCredentialValidator.d.ts +19 -0
  660. package/dist/sync/coordinator/workspaceSyncCloudCredentialValidator.js +83 -0
  661. package/dist/sync/coordinator/workspaceSyncCredentialBroker.d.ts +117 -0
  662. package/dist/sync/coordinator/workspaceSyncCredentialBroker.js +323 -0
  663. package/dist/sync/coordinator/workspaceSyncLocalAccessBootstrap.d.ts +11 -0
  664. package/dist/sync/coordinator/workspaceSyncLocalAccessBootstrap.js +51 -0
  665. package/dist/sync/coordinator/workspaceSyncTransportGateway.d.ts +139 -0
  666. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +738 -0
  667. package/dist/sync/engine/localWorkspaceContentManifestService.d.ts +21 -0
  668. package/dist/sync/engine/localWorkspaceContentManifestService.js +44 -0
  669. package/dist/sync/engine/workspaceSyncAtomicLocalPublisher.d.ts +36 -0
  670. package/dist/sync/engine/workspaceSyncAtomicLocalPublisher.js +203 -0
  671. package/dist/sync/engine/workspaceSyncContentLifecycleAdapter.d.ts +21 -0
  672. package/dist/sync/engine/workspaceSyncContentLifecycleAdapter.js +13 -0
  673. package/dist/sync/engine/workspaceSyncContentPullObligationStore.d.ts +65 -0
  674. package/dist/sync/engine/workspaceSyncContentPullObligationStore.js +160 -0
  675. package/dist/sync/engine/workspaceSyncContentVersionStore.d.ts +216 -0
  676. package/dist/sync/engine/workspaceSyncContentVersionStore.js +1068 -0
  677. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.d.ts +28 -0
  678. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +543 -0
  679. package/dist/sync/engine/workspaceSyncEngineContracts.d.ts +78 -0
  680. package/dist/sync/engine/workspaceSyncEngineContracts.js +1 -0
  681. package/dist/sync/engine/workspaceSyncEngineControlPlane.d.ts +37 -0
  682. package/dist/sync/engine/workspaceSyncEngineControlPlane.js +79 -0
  683. package/dist/sync/engine/workspaceSyncEngineIdentity.d.ts +1 -0
  684. package/dist/sync/engine/workspaceSyncEngineIdentity.js +4 -0
  685. package/dist/sync/engine/workspaceSyncEngineLifecyclePolicy.d.ts +68 -0
  686. package/dist/sync/engine/workspaceSyncEngineLifecyclePolicy.js +180 -0
  687. package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +72 -0
  688. package/dist/sync/engine/workspaceSyncEngineLocalApply.js +132 -0
  689. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +70 -0
  690. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +25 -0
  691. package/dist/sync/engine/workspaceSyncEngineParity.d.ts +8 -0
  692. package/dist/sync/engine/workspaceSyncEngineParity.js +7 -0
  693. package/dist/sync/engine/workspaceSyncEnginePullPolicy.d.ts +32 -0
  694. package/dist/sync/engine/workspaceSyncEnginePullPolicy.js +27 -0
  695. package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +20 -0
  696. package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +87 -0
  697. package/dist/sync/engine/workspaceSyncEngineRecovery.d.ts +56 -0
  698. package/dist/sync/engine/workspaceSyncEngineRecovery.js +288 -0
  699. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +104 -0
  700. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +21 -0
  701. package/dist/sync/engine/workspaceSyncEngineRuntime.d.ts +61 -0
  702. package/dist/sync/engine/workspaceSyncEngineRuntime.js +59 -0
  703. package/dist/sync/engine/workspaceSyncEngineServerApplyBoundary.d.ts +28 -0
  704. package/dist/sync/engine/workspaceSyncEngineServerApplyBoundary.js +86 -0
  705. package/dist/sync/engine/workspaceSyncEngineServerLocalServices.d.ts +73 -0
  706. package/dist/sync/engine/workspaceSyncEngineServerLocalServices.js +452 -0
  707. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +23 -0
  708. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +451 -0
  709. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +125 -0
  710. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +1124 -0
  711. package/dist/sync/engine/workspaceSyncEngineServerSnapshotReader.d.ts +63 -0
  712. package/dist/sync/engine/workspaceSyncEngineServerSnapshotReader.js +276 -0
  713. package/dist/sync/engine/workspaceSyncEngineTransferCompletion.d.ts +20 -0
  714. package/dist/sync/engine/workspaceSyncEngineTransferCompletion.js +37 -0
  715. package/dist/sync/engine/workspaceSyncEngineTransferTypes.d.ts +7 -0
  716. package/dist/sync/engine/workspaceSyncEngineTransferTypes.js +1 -0
  717. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +240 -0
  718. package/dist/sync/engine/workspaceSyncEngineTransfers.js +1184 -0
  719. package/dist/sync/engine/workspaceSyncEngineV3LocalFeedApply.d.ts +39 -0
  720. package/dist/sync/engine/workspaceSyncEngineV3LocalFeedApply.js +39 -0
  721. package/dist/sync/engine/workspaceSyncImmutableContentGcRunner.d.ts +25 -0
  722. package/dist/sync/engine/workspaceSyncImmutableContentGcRunner.js +83 -0
  723. package/dist/sync/engine/workspaceSyncImmutableContentProvider.d.ts +71 -0
  724. package/dist/sync/engine/workspaceSyncImmutableContentProvider.js +209 -0
  725. package/dist/sync/engine/workspaceSyncImmutableContentStager.d.ts +32 -0
  726. package/dist/sync/engine/workspaceSyncImmutableContentStager.js +202 -0
  727. package/dist/sync/engine/workspaceSyncPublishedContentResolver.d.ts +43 -0
  728. package/dist/sync/engine/workspaceSyncPublishedContentResolver.js +142 -0
  729. package/dist/sync/engine/workspaceSyncServerContentRuntime.d.ts +26 -0
  730. package/dist/sync/engine/workspaceSyncServerContentRuntime.js +41 -0
  731. package/dist/sync/engine/workspaceSyncServerDataServices.d.ts +44 -0
  732. package/dist/sync/engine/workspaceSyncServerDataServices.js +143 -0
  733. package/dist/sync/engine/workspaceSyncTransferEvidence.d.ts +39 -0
  734. package/dist/sync/engine/workspaceSyncTransferEvidence.js +45 -0
  735. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +44 -0
  736. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +990 -0
  737. package/dist/sync/engine/workspaceSyncV2PreparedContent.d.ts +82 -0
  738. package/dist/sync/engine/workspaceSyncV2PreparedContent.js +219 -0
  739. package/dist/sync/planningCanonicalFields.d.ts +41 -0
  740. package/dist/sync/planningCanonicalFields.js +211 -0
  741. package/dist/sync/planningSyncPayload.d.ts +5 -1
  742. package/dist/sync/planningSyncPayload.js +88 -4
  743. package/dist/sync/syncApplyHandlers.d.ts +98 -8
  744. package/dist/sync/syncApplyHandlers.js +816 -78
  745. package/dist/sync/syncResourceAdapters.d.ts +10 -0
  746. package/dist/sync/syncResourceAdapters.js +21 -1
  747. package/dist/sync/syncService.d.ts +57 -1
  748. package/dist/sync/syncService.js +1085 -138
  749. package/dist/sync/taskNestedContent.d.ts +2 -0
  750. package/dist/sync/taskNestedContent.js +8 -0
  751. package/dist/sync/taskSyncChangeKey.js +0 -1
  752. package/dist/sync/taskSyncPayload.d.ts +0 -1
  753. package/dist/sync/taskSyncPayload.js +14 -3
  754. package/dist/sync/v3/canonicalWorkspaceSyncJson.d.ts +2 -0
  755. package/dist/sync/v3/canonicalWorkspaceSyncJson.js +26 -0
  756. package/dist/sync/v3/combinedFeedCapability.d.ts +21 -0
  757. package/dist/sync/v3/combinedFeedCapability.js +80 -0
  758. package/dist/sync/v3/combinedFeedPrerequisites.d.ts +18 -0
  759. package/dist/sync/v3/combinedFeedPrerequisites.js +184 -0
  760. package/dist/sync/v3/durableInitiativeMutationRouter.d.ts +61 -0
  761. package/dist/sync/v3/durableInitiativeMutationRouter.js +287 -0
  762. package/dist/sync/v3/durableOperationReplay.d.ts +36 -0
  763. package/dist/sync/v3/durableOperationReplay.js +103 -0
  764. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +49 -0
  765. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +198 -0
  766. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +47 -0
  767. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +182 -0
  768. package/dist/sync/v3/durableTaskCommentMutationRouter.d.ts +47 -0
  769. package/dist/sync/v3/durableTaskCommentMutationRouter.js +191 -0
  770. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +60 -0
  771. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +320 -0
  772. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +40 -0
  773. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +518 -0
  774. package/dist/sync/v3/durableTaxonomyMutationRouter.d.ts +63 -0
  775. package/dist/sync/v3/durableTaxonomyMutationRouter.js +289 -0
  776. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +77 -0
  777. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +361 -0
  778. package/dist/sync/v3/durableWorkstreamMutationRouter.d.ts +61 -0
  779. package/dist/sync/v3/durableWorkstreamMutationRouter.js +287 -0
  780. package/dist/sync/v3/durableWorkstreamTaskOrderMutationRouter.d.ts +42 -0
  781. package/dist/sync/v3/durableWorkstreamTaskOrderMutationRouter.js +122 -0
  782. package/dist/sync/v3/entityEventLegacyAdoptionContract.d.ts +6 -0
  783. package/dist/sync/v3/entityEventLegacyAdoptionContract.js +6 -0
  784. package/dist/sync/v3/entityEventLegacyAdoptionService.d.ts +62 -0
  785. package/dist/sync/v3/entityEventLegacyAdoptionService.js +180 -0
  786. package/dist/sync/v3/initiativeActivityAuthoritativeRevision.d.ts +11 -0
  787. package/dist/sync/v3/initiativeActivityAuthoritativeRevision.js +23 -0
  788. package/dist/sync/v3/initiativeActivityIdentity.d.ts +8 -0
  789. package/dist/sync/v3/initiativeActivityIdentity.js +14 -0
  790. package/dist/sync/v3/initiativeCoexistence.d.ts +26 -0
  791. package/dist/sync/v3/initiativeCoexistence.js +148 -0
  792. package/dist/sync/v3/initiativeFeedActivationCapability.d.ts +6 -0
  793. package/dist/sync/v3/initiativeFeedActivationCapability.js +16 -0
  794. package/dist/sync/v3/initiativeMutationPayload.d.ts +26 -0
  795. package/dist/sync/v3/initiativeMutationPayload.js +144 -0
  796. package/dist/sync/v3/initiativeMutationService.d.ts +62 -0
  797. package/dist/sync/v3/initiativeMutationService.js +206 -0
  798. package/dist/sync/v3/localEntityEventLegacyAdoption.d.ts +31 -0
  799. package/dist/sync/v3/localEntityEventLegacyAdoption.js +607 -0
  800. package/dist/sync/v3/localInitiativeOutboxHandler.d.ts +8 -0
  801. package/dist/sync/v3/localInitiativeOutboxHandler.js +88 -0
  802. package/dist/sync/v3/localInitiativeOutboxService.d.ts +50 -0
  803. package/dist/sync/v3/localInitiativeOutboxService.js +495 -0
  804. package/dist/sync/v3/localOperationIdentityStore.d.ts +81 -0
  805. package/dist/sync/v3/localOperationIdentityStore.js +388 -0
  806. package/dist/sync/v3/localOutboxDispatchCapability.d.ts +7 -0
  807. package/dist/sync/v3/localOutboxDispatchCapability.js +13 -0
  808. package/dist/sync/v3/localOutboxDispatchRuntime.d.ts +16 -0
  809. package/dist/sync/v3/localOutboxDispatchRuntime.js +269 -0
  810. package/dist/sync/v3/localOutboxDispatcher.d.ts +53 -0
  811. package/dist/sync/v3/localOutboxDispatcher.js +362 -0
  812. package/dist/sync/v3/localOutboxDrainClient.d.ts +50 -0
  813. package/dist/sync/v3/localOutboxDrainClient.js +250 -0
  814. package/dist/sync/v3/localOutboxOperationHandler.d.ts +31 -0
  815. package/dist/sync/v3/localOutboxOperationHandler.js +10 -0
  816. package/dist/sync/v3/localOutboxRecoveryClient.d.ts +14 -0
  817. package/dist/sync/v3/localOutboxRecoveryClient.js +45 -0
  818. package/dist/sync/v3/localOutboxRecoveryPreparationService.d.ts +12 -0
  819. package/dist/sync/v3/localOutboxRecoveryPreparationService.js +151 -0
  820. package/dist/sync/v3/localSyncIdentityStore.d.ts +9 -0
  821. package/dist/sync/v3/localSyncIdentityStore.js +43 -0
  822. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.d.ts +10 -0
  823. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +70 -0
  824. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +45 -0
  825. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +291 -0
  826. package/dist/sync/v3/localTaskChecklistOutboxHandler.d.ts +10 -0
  827. package/dist/sync/v3/localTaskChecklistOutboxHandler.js +54 -0
  828. package/dist/sync/v3/localTaskChecklistOutboxService.d.ts +47 -0
  829. package/dist/sync/v3/localTaskChecklistOutboxService.js +221 -0
  830. package/dist/sync/v3/localTaskCommentOutboxHandler.d.ts +10 -0
  831. package/dist/sync/v3/localTaskCommentOutboxHandler.js +94 -0
  832. package/dist/sync/v3/localTaskCommentOutboxService.d.ts +75 -0
  833. package/dist/sync/v3/localTaskCommentOutboxService.js +391 -0
  834. package/dist/sync/v3/localTaskCreateClient.d.ts +27 -0
  835. package/dist/sync/v3/localTaskCreateClient.js +209 -0
  836. package/dist/sync/v3/localTaskCreateOutboxHandler.d.ts +8 -0
  837. package/dist/sync/v3/localTaskCreateOutboxHandler.js +147 -0
  838. package/dist/sync/v3/localTaskCreateOutboxService.d.ts +77 -0
  839. package/dist/sync/v3/localTaskCreateOutboxService.js +397 -0
  840. package/dist/sync/v3/localTaskDeleteClient.d.ts +10 -0
  841. package/dist/sync/v3/localTaskDeleteClient.js +23 -0
  842. package/dist/sync/v3/localTaskDeleteOutboxHandler.d.ts +8 -0
  843. package/dist/sync/v3/localTaskDeleteOutboxHandler.js +54 -0
  844. package/dist/sync/v3/localTaskDeleteOutboxService.d.ts +38 -0
  845. package/dist/sync/v3/localTaskDeleteOutboxService.js +235 -0
  846. package/dist/sync/v3/localTaskLegacyRootAdoption.d.ts +28 -0
  847. package/dist/sync/v3/localTaskLegacyRootAdoption.js +219 -0
  848. package/dist/sync/v3/localTaskLifecycleClient.d.ts +49 -0
  849. package/dist/sync/v3/localTaskLifecycleClient.js +57 -0
  850. package/dist/sync/v3/localTaskLifecycleOutboxHandler.d.ts +8 -0
  851. package/dist/sync/v3/localTaskLifecycleOutboxHandler.js +94 -0
  852. package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +60 -0
  853. package/dist/sync/v3/localTaskLifecycleOutboxService.js +304 -0
  854. package/dist/sync/v3/localTaskMetadataClient.d.ts +13 -0
  855. package/dist/sync/v3/localTaskMetadataClient.js +30 -0
  856. package/dist/sync/v3/localTaskMetadataOutboxHandler.d.ts +8 -0
  857. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +70 -0
  858. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +48 -0
  859. package/dist/sync/v3/localTaskMetadataOutboxService.js +229 -0
  860. package/dist/sync/v3/localTaskOperationClient.d.ts +72 -0
  861. package/dist/sync/v3/localTaskOperationClient.js +195 -0
  862. package/dist/sync/v3/localTaskRelationshipOutboxHandler.d.ts +8 -0
  863. package/dist/sync/v3/localTaskRelationshipOutboxHandler.js +114 -0
  864. package/dist/sync/v3/localTaskRelationshipOutboxService.d.ts +45 -0
  865. package/dist/sync/v3/localTaskRelationshipOutboxService.js +192 -0
  866. package/dist/sync/v3/localTaskRestoreClient.d.ts +13 -0
  867. package/dist/sync/v3/localTaskRestoreClient.js +32 -0
  868. package/dist/sync/v3/localTaskRestoreOutboxHandler.d.ts +8 -0
  869. package/dist/sync/v3/localTaskRestoreOutboxHandler.js +41 -0
  870. package/dist/sync/v3/localTaskRestoreOutboxService.d.ts +56 -0
  871. package/dist/sync/v3/localTaskRestoreOutboxService.js +323 -0
  872. package/dist/sync/v3/localTaskStatusClient.d.ts +17 -0
  873. package/dist/sync/v3/localTaskStatusClient.js +35 -0
  874. package/dist/sync/v3/localTaskStatusOutboxHandler.d.ts +8 -0
  875. package/dist/sync/v3/localTaskStatusOutboxHandler.js +75 -0
  876. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +53 -0
  877. package/dist/sync/v3/localTaskStatusOutboxService.js +422 -0
  878. package/dist/sync/v3/localTaxonomyCompositeOutboxService.d.ts +34 -0
  879. package/dist/sync/v3/localTaxonomyCompositeOutboxService.js +290 -0
  880. package/dist/sync/v3/localTaxonomyOutboxHandler.d.ts +9 -0
  881. package/dist/sync/v3/localTaxonomyOutboxHandler.js +106 -0
  882. package/dist/sync/v3/localTaxonomyOutboxService.d.ts +30 -0
  883. package/dist/sync/v3/localTaxonomyOutboxService.js +181 -0
  884. package/dist/sync/v3/localWorkflowRuntimeOutboxHandler.d.ts +8 -0
  885. package/dist/sync/v3/localWorkflowRuntimeOutboxHandler.js +113 -0
  886. package/dist/sync/v3/localWorkflowRuntimeOutboxService.d.ts +37 -0
  887. package/dist/sync/v3/localWorkflowRuntimeOutboxService.js +330 -0
  888. package/dist/sync/v3/localWorkspaceSyncServerOutboxDispatcher.d.ts +63 -0
  889. package/dist/sync/v3/localWorkspaceSyncServerOutboxDispatcher.js +264 -0
  890. package/dist/sync/v3/localWorkstreamOutboxHandler.d.ts +8 -0
  891. package/dist/sync/v3/localWorkstreamOutboxHandler.js +102 -0
  892. package/dist/sync/v3/localWorkstreamOutboxService.d.ts +52 -0
  893. package/dist/sync/v3/localWorkstreamOutboxService.js +579 -0
  894. package/dist/sync/v3/localWorkstreamTaskOrderOutboxHandler.d.ts +10 -0
  895. package/dist/sync/v3/localWorkstreamTaskOrderOutboxHandler.js +57 -0
  896. package/dist/sync/v3/localWorkstreamTaskOrderOutboxService.d.ts +30 -0
  897. package/dist/sync/v3/localWorkstreamTaskOrderOutboxService.js +128 -0
  898. package/dist/sync/v3/planningActivation.d.ts +13 -0
  899. package/dist/sync/v3/planningActivation.js +33 -0
  900. package/dist/sync/v3/planningAttachmentActivityValidation.d.ts +9 -0
  901. package/dist/sync/v3/planningAttachmentActivityValidation.js +87 -0
  902. package/dist/sync/v3/programScaffoldService.d.ts +143 -0
  903. package/dist/sync/v3/programScaffoldService.js +478 -0
  904. package/dist/sync/v3/syncDurabilityStore.d.ts +773 -0
  905. package/dist/sync/v3/syncDurabilityStore.js +4635 -0
  906. package/dist/sync/v3/taskActivityAuthoritativeRevision.d.ts +16 -0
  907. package/dist/sync/v3/taskActivityAuthoritativeRevision.js +67 -0
  908. package/dist/sync/v3/taskActivityJournal.d.ts +20 -0
  909. package/dist/sync/v3/taskActivityJournal.js +195 -0
  910. package/dist/sync/v3/taskAttachmentLinksActivityIdentity.d.ts +6 -0
  911. package/dist/sync/v3/taskAttachmentLinksActivityIdentity.js +14 -0
  912. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +111 -0
  913. package/dist/sync/v3/taskAttachmentLinksMutationService.js +202 -0
  914. package/dist/sync/v3/taskChecklistActivityIdentity.d.ts +6 -0
  915. package/dist/sync/v3/taskChecklistActivityIdentity.js +14 -0
  916. package/dist/sync/v3/taskChecklistMutationService.d.ts +92 -0
  917. package/dist/sync/v3/taskChecklistMutationService.js +156 -0
  918. package/dist/sync/v3/taskCommentActivityIdentity.d.ts +7 -0
  919. package/dist/sync/v3/taskCommentActivityIdentity.js +15 -0
  920. package/dist/sync/v3/taskCommentMutationService.d.ts +150 -0
  921. package/dist/sync/v3/taskCommentMutationService.js +392 -0
  922. package/dist/sync/v3/taskCommentReconcileProvenance.d.ts +58 -0
  923. package/dist/sync/v3/taskCommentReconcileProvenance.js +170 -0
  924. package/dist/sync/v3/taskCreateActivation.d.ts +19 -0
  925. package/dist/sync/v3/taskCreateActivation.js +84 -0
  926. package/dist/sync/v3/taskCreateActivityIdentity.d.ts +7 -0
  927. package/dist/sync/v3/taskCreateActivityIdentity.js +18 -0
  928. package/dist/sync/v3/taskCreateMutationService.d.ts +103 -0
  929. package/dist/sync/v3/taskCreateMutationService.js +283 -0
  930. package/dist/sync/v3/taskDeleteActivation.d.ts +7 -0
  931. package/dist/sync/v3/taskDeleteActivation.js +14 -0
  932. package/dist/sync/v3/taskDeleteMutationService.d.ts +35 -0
  933. package/dist/sync/v3/taskDeleteMutationService.js +133 -0
  934. package/dist/sync/v3/taskLegacyRootAdoptionService.d.ts +78 -0
  935. package/dist/sync/v3/taskLegacyRootAdoptionService.js +175 -0
  936. package/dist/sync/v3/taskLifecycleActivityIdentity.d.ts +9 -0
  937. package/dist/sync/v3/taskLifecycleActivityIdentity.js +18 -0
  938. package/dist/sync/v3/taskLifecycleMutationPayload.d.ts +5 -0
  939. package/dist/sync/v3/taskLifecycleMutationPayload.js +28 -0
  940. package/dist/sync/v3/taskLifecycleMutationService.d.ts +66 -0
  941. package/dist/sync/v3/taskLifecycleMutationService.js +92 -0
  942. package/dist/sync/v3/taskMetadataActivation.d.ts +12 -0
  943. package/dist/sync/v3/taskMetadataActivation.js +38 -0
  944. package/dist/sync/v3/taskMetadataActivityIdentity.d.ts +7 -0
  945. package/dist/sync/v3/taskMetadataActivityIdentity.js +18 -0
  946. package/dist/sync/v3/taskMetadataCoexistence.d.ts +37 -0
  947. package/dist/sync/v3/taskMetadataCoexistence.js +134 -0
  948. package/dist/sync/v3/taskMetadataMutationService.d.ts +71 -0
  949. package/dist/sync/v3/taskMetadataMutationService.js +199 -0
  950. package/dist/sync/v3/taskRelationshipLegacyAdoptionService.d.ts +48 -0
  951. package/dist/sync/v3/taskRelationshipLegacyAdoptionService.js +164 -0
  952. package/dist/sync/v3/taskRelationshipMutationService.d.ts +50 -0
  953. package/dist/sync/v3/taskRelationshipMutationService.js +148 -0
  954. package/dist/sync/v3/taskRestoreMutationService.d.ts +90 -0
  955. package/dist/sync/v3/taskRestoreMutationService.js +179 -0
  956. package/dist/sync/v3/taskStatusActivation.d.ts +7 -0
  957. package/dist/sync/v3/taskStatusActivation.js +15 -0
  958. package/dist/sync/v3/taskStatusMutationService.d.ts +76 -0
  959. package/dist/sync/v3/taskStatusMutationService.js +308 -0
  960. package/dist/sync/v3/taskWorkflowMutationPayload.d.ts +4 -0
  961. package/dist/sync/v3/taskWorkflowMutationPayload.js +22 -0
  962. package/dist/sync/v3/taskWorkflowMutationService.d.ts +64 -0
  963. package/dist/sync/v3/taskWorkflowMutationService.js +102 -0
  964. package/dist/sync/v3/taxonomyCoexistence.d.ts +10 -0
  965. package/dist/sync/v3/taxonomyCoexistence.js +19 -0
  966. package/dist/sync/v3/taxonomyCompositeMutationService.d.ts +110 -0
  967. package/dist/sync/v3/taxonomyCompositeMutationService.js +304 -0
  968. package/dist/sync/v3/taxonomyMutationService.d.ts +57 -0
  969. package/dist/sync/v3/taxonomyMutationService.js +106 -0
  970. package/dist/sync/v3/taxonomyStateMutation.d.ts +22 -0
  971. package/dist/sync/v3/taxonomyStateMutation.js +49 -0
  972. package/dist/sync/v3/taxonomySyncPayload.d.ts +9 -0
  973. package/dist/sync/v3/taxonomySyncPayload.js +199 -0
  974. package/dist/sync/v3/workflowAssignmentSync.d.ts +22 -0
  975. package/dist/sync/v3/workflowAssignmentSync.js +213 -0
  976. package/dist/sync/v3/workflowRuntimeMutationService.d.ts +89 -0
  977. package/dist/sync/v3/workflowRuntimeMutationService.js +359 -0
  978. package/dist/sync/v3/workflowRuntimeOperationIdentity.d.ts +13 -0
  979. package/dist/sync/v3/workflowRuntimeOperationIdentity.js +35 -0
  980. package/dist/sync/v3/workflowRuntimeSync.d.ts +141 -0
  981. package/dist/sync/v3/workflowRuntimeSync.js +485 -0
  982. package/dist/sync/v3/workspaceSyncJournalReader.d.ts +43 -0
  983. package/dist/sync/v3/workspaceSyncJournalReader.js +45 -0
  984. package/dist/sync/v3/workspaceSyncLocalOwnershipSnapshot.d.ts +28 -0
  985. package/dist/sync/v3/workspaceSyncLocalOwnershipSnapshot.js +81 -0
  986. package/dist/sync/v3/workspaceSyncPayloadFingerprint.d.ts +1 -0
  987. package/dist/sync/v3/workspaceSyncPayloadFingerprint.js +5 -0
  988. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +16 -0
  989. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +161 -0
  990. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +83 -0
  991. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +161 -0
  992. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +16 -0
  993. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +90 -0
  994. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.d.ts +65 -0
  995. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +356 -0
  996. package/dist/sync/v3/workspaceSyncV3CombinedCoverage.d.ts +51 -0
  997. package/dist/sync/v3/workspaceSyncV3CombinedCoverage.js +171 -0
  998. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.d.ts +43 -0
  999. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +535 -0
  1000. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.d.ts +50 -0
  1001. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +1149 -0
  1002. package/dist/sync/v3/workspaceSyncV3CombinedProjectionState.d.ts +96 -0
  1003. package/dist/sync/v3/workspaceSyncV3CombinedProjectionState.js +672 -0
  1004. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +42 -0
  1005. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +598 -0
  1006. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +65 -0
  1007. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +505 -0
  1008. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +78 -0
  1009. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +511 -0
  1010. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +45 -0
  1011. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +125 -0
  1012. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.d.ts +13 -0
  1013. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +34 -0
  1014. package/dist/sync/v3/workspaceSyncV3CursorEncoding.d.ts +1 -0
  1015. package/dist/sync/v3/workspaceSyncV3CursorEncoding.js +6 -0
  1016. package/dist/sync/v3/workspaceSyncV3DomainAdmission.d.ts +43 -0
  1017. package/dist/sync/v3/workspaceSyncV3DomainAdmission.js +118 -0
  1018. package/dist/sync/v3/workspaceSyncV3Feed.d.ts +95 -0
  1019. package/dist/sync/v3/workspaceSyncV3Feed.js +243 -0
  1020. package/dist/sync/v3/workspaceSyncV3FeedApply.d.ts +13 -0
  1021. package/dist/sync/v3/workspaceSyncV3FeedApply.js +67 -0
  1022. package/dist/sync/v3/workspaceSyncV3FeedClientContract.d.ts +3 -0
  1023. package/dist/sync/v3/workspaceSyncV3FeedClientContract.js +46 -0
  1024. package/dist/sync/v3/workspaceSyncV3FeedCursorKeys.d.ts +10 -0
  1025. package/dist/sync/v3/workspaceSyncV3FeedCursorKeys.js +58 -0
  1026. package/dist/sync/v3/workspaceSyncV3FeedOrchestrator.d.ts +31 -0
  1027. package/dist/sync/v3/workspaceSyncV3FeedOrchestrator.js +228 -0
  1028. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +32 -0
  1029. package/dist/sync/v3/workspaceSyncV3FeedProfile.js +21 -0
  1030. package/dist/sync/v3/workspaceSyncV3InitiativeLocalApply.d.ts +36 -0
  1031. package/dist/sync/v3/workspaceSyncV3InitiativeLocalApply.js +163 -0
  1032. package/dist/sync/v3/workspaceSyncV3InitiativeRepair.d.ts +33 -0
  1033. package/dist/sync/v3/workspaceSyncV3InitiativeRepair.js +118 -0
  1034. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +10 -0
  1035. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +45 -0
  1036. package/dist/sync/v3/workspaceSyncV3LocalOutboxStatus.d.ts +27 -0
  1037. package/dist/sync/v3/workspaceSyncV3LocalOutboxStatus.js +83 -0
  1038. package/dist/sync/v3/workspaceSyncV3NestedCoverage.d.ts +10 -0
  1039. package/dist/sync/v3/workspaceSyncV3NestedCoverage.js +40 -0
  1040. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +83 -0
  1041. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +325 -0
  1042. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +59 -0
  1043. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +302 -0
  1044. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +47 -0
  1045. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +329 -0
  1046. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +19 -0
  1047. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +41 -0
  1048. package/dist/sync/v3/workspaceSyncV3NestedWireContract.d.ts +6 -0
  1049. package/dist/sync/v3/workspaceSyncV3NestedWireContract.js +16 -0
  1050. package/dist/sync/v3/workspaceSyncV3OperationProjection.d.ts +8 -0
  1051. package/dist/sync/v3/workspaceSyncV3OperationProjection.js +52 -0
  1052. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +229 -0
  1053. package/dist/sync/v3/workspaceSyncV3OperationProtocol.js +1 -0
  1054. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +165 -0
  1055. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +414 -0
  1056. package/dist/sync/v3/workspaceSyncV3OriginActor.d.ts +36 -0
  1057. package/dist/sync/v3/workspaceSyncV3OriginActor.js +61 -0
  1058. package/dist/sync/v3/workspaceSyncV3OriginAssertion.d.ts +46 -0
  1059. package/dist/sync/v3/workspaceSyncV3OriginAssertion.js +121 -0
  1060. package/dist/sync/v3/workspaceSyncV3OriginAssertionHeader.d.ts +1 -0
  1061. package/dist/sync/v3/workspaceSyncV3OriginAssertionHeader.js +1 -0
  1062. package/dist/sync/v3/workspaceSyncV3PlanningOrderCoverage.d.ts +10 -0
  1063. package/dist/sync/v3/workspaceSyncV3PlanningOrderCoverage.js +34 -0
  1064. package/dist/sync/v3/workspaceSyncV3PlanningOrderWireContract.d.ts +6 -0
  1065. package/dist/sync/v3/workspaceSyncV3PlanningOrderWireContract.js +10 -0
  1066. package/dist/sync/v3/workspaceSyncV3ProjectionState.d.ts +105 -0
  1067. package/dist/sync/v3/workspaceSyncV3ProjectionState.js +467 -0
  1068. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.d.ts +56 -0
  1069. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +250 -0
  1070. package/dist/sync/v3/workspaceSyncV3TaskRelationshipProjection.d.ts +30 -0
  1071. package/dist/sync/v3/workspaceSyncV3TaskRelationshipProjection.js +142 -0
  1072. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +40 -0
  1073. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +246 -0
  1074. package/dist/sync/v3/workspaceSyncV3TaxonomyFeedProjection.d.ts +39 -0
  1075. package/dist/sync/v3/workspaceSyncV3TaxonomyFeedProjection.js +185 -0
  1076. package/dist/sync/v3/workspaceSyncV3WorkflowCoverage.d.ts +10 -0
  1077. package/dist/sync/v3/workspaceSyncV3WorkflowCoverage.js +36 -0
  1078. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +17 -0
  1079. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +143 -0
  1080. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeProjection.d.ts +9 -0
  1081. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeProjection.js +195 -0
  1082. package/dist/sync/v3/workspaceSyncV3WorkflowWireContract.d.ts +11 -0
  1083. package/dist/sync/v3/workspaceSyncV3WorkflowWireContract.js +12 -0
  1084. package/dist/sync/v3/workspaceSyncV3WorkstreamProjection.d.ts +4 -0
  1085. package/dist/sync/v3/workspaceSyncV3WorkstreamProjection.js +98 -0
  1086. package/dist/sync/v3/workspaceSyncV3WorkstreamTaskOrderProjection.d.ts +9 -0
  1087. package/dist/sync/v3/workspaceSyncV3WorkstreamTaskOrderProjection.js +72 -0
  1088. package/dist/sync/v3/workstreamActivityAuthoritativeRevision.d.ts +11 -0
  1089. package/dist/sync/v3/workstreamActivityAuthoritativeRevision.js +23 -0
  1090. package/dist/sync/v3/workstreamActivityIdentity.d.ts +8 -0
  1091. package/dist/sync/v3/workstreamActivityIdentity.js +14 -0
  1092. package/dist/sync/v3/workstreamCoexistence.d.ts +19 -0
  1093. package/dist/sync/v3/workstreamCoexistence.js +64 -0
  1094. package/dist/sync/v3/workstreamMutationPayload.d.ts +27 -0
  1095. package/dist/sync/v3/workstreamMutationPayload.js +147 -0
  1096. package/dist/sync/v3/workstreamMutationService.d.ts +72 -0
  1097. package/dist/sync/v3/workstreamMutationService.js +256 -0
  1098. package/dist/sync/v3/workstreamTaskOrderMutationService.d.ts +55 -0
  1099. package/dist/sync/v3/workstreamTaskOrderMutationService.js +110 -0
  1100. package/dist/sync/v3/workstreamTaskOrderSync.d.ts +14 -0
  1101. package/dist/sync/v3/workstreamTaskOrderSync.js +25 -0
  1102. package/dist/sync/workspaceContentManifest.d.ts +86 -0
  1103. package/dist/sync/workspaceContentManifest.js +123 -0
  1104. package/dist/sync/workspacePullFeed.d.ts +186 -2
  1105. package/dist/sync/workspacePullFeed.js +513 -136
  1106. package/dist/sync/workspacePushPreparedPlan.d.ts +21 -0
  1107. package/dist/sync/workspacePushPreparedPlan.js +1 -0
  1108. package/dist/sync/workspaceRepair.d.ts +17 -25
  1109. package/dist/sync/workspaceRepair.js +348 -25
  1110. package/dist/sync/workspaceSyncContentDecryption.d.ts +6 -0
  1111. package/dist/sync/workspaceSyncContentDecryption.js +11 -0
  1112. package/dist/sync/workspaceSyncModel.d.ts +216 -5
  1113. package/dist/sync/workspaceSyncModel.js +371 -35
  1114. package/dist/sync/workspaceSyncSnapshotProjection.d.ts +11 -0
  1115. package/dist/sync/workspaceSyncSnapshotProjection.js +101 -0
  1116. package/dist/sync/workspaceSyncState.d.ts +13 -0
  1117. package/dist/sync/workspaceSyncState.js +36 -0
  1118. package/dist/sync/workspaceSyncV2SnapshotBuilder.d.ts +106 -0
  1119. package/dist/sync/workspaceSyncV2SnapshotBuilder.js +193 -0
  1120. package/dist/types.d.ts +23 -15
  1121. package/dist/types.js +2 -4
  1122. package/dist/ui/ai-profile-clients/amazon-nova.svg +1 -0
  1123. package/dist/ui/ai-profile-clients/antigravity.png +0 -0
  1124. package/dist/ui/ai-profile-clients/chatgpt.svg +10 -0
  1125. package/dist/ui/ai-profile-clients/claude-chat.svg +1 -0
  1126. package/dist/ui/ai-profile-clients/claude-code.svg +15 -0
  1127. package/dist/ui/ai-profile-clients/cline-app-icon-light.png +0 -0
  1128. package/dist/ui/ai-profile-clients/cline.svg +16 -0
  1129. package/dist/ui/ai-profile-clients/codex.svg +1 -0
  1130. package/dist/ui/ai-profile-clients/copilot.png +0 -0
  1131. package/dist/ui/ai-profile-clients/cursor.svg +1 -0
  1132. package/dist/ui/ai-profile-clients/deepseek.svg +1 -0
  1133. package/dist/ui/ai-profile-clients/gemini-cli-icon.png +0 -0
  1134. package/dist/ui/ai-profile-clients/gemini.svg +10 -0
  1135. package/dist/ui/ai-profile-clients/grok.svg +31 -0
  1136. package/dist/ui/ai-profile-clients/kiro.svg +11 -0
  1137. package/dist/ui/ai-profile-clients/llama.svg +1 -0
  1138. package/dist/ui/ai-profile-clients/mistral.svg +1 -0
  1139. package/dist/ui/ai-profile-clients/ms_copilot.jpeg +0 -0
  1140. package/dist/ui/ai-profile-clients/openclaw.png +0 -0
  1141. package/dist/ui/ai-profile-clients/perplexity.svg +1 -0
  1142. package/dist/ui/ai-profile-clients/vscode.svg +41 -0
  1143. package/dist/ui/ai-profile-clients/windsurf.svg +11 -0
  1144. package/dist/ui/assets/AiIdentityRosterCard-DB1BLO_j.js +1 -0
  1145. package/dist/ui/assets/AiProfilesModule-DPxQeF9N.js +1 -0
  1146. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +1 -0
  1147. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DHNYaNg6.js +3 -0
  1148. package/dist/ui/assets/AssetTraySortControl-B9k5caQW.css +1 -0
  1149. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +1 -0
  1150. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +3 -0
  1151. package/dist/ui/assets/DocumentWorkspace-BC7sy5TG.js +261 -0
  1152. package/dist/ui/assets/DocumentWorkspace-BIBGBfd7.css +1 -0
  1153. package/dist/ui/assets/EntityActivityTimeline-BfJbZRdl.css +1 -0
  1154. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +1 -0
  1155. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +1 -0
  1156. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +1 -0
  1157. package/dist/ui/assets/PlansPage-BVH_NfgT.js +1 -0
  1158. package/dist/ui/assets/PlansPage-C2nkK2Bp.css +1 -0
  1159. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +1 -0
  1160. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +1 -0
  1161. package/dist/ui/assets/TaskSettings-xVsxSa0g.js +12 -0
  1162. package/dist/ui/assets/TaskforceAgentsModule-CkAxW19K.js +21 -0
  1163. package/dist/ui/assets/TaskforceAgentsModule-D6IC0S7Q.css +1 -0
  1164. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +1 -0
  1165. package/dist/ui/assets/WorkflowManagerModule-DF10XENz.css +1 -0
  1166. package/dist/ui/assets/antigravity-DRKQga2U.png +0 -0
  1167. package/dist/ui/assets/cursor-BM_3k4Y4.svg +1 -0
  1168. package/dist/ui/assets/gemini-cli-icon-dBAVjCcn.png +0 -0
  1169. package/dist/ui/assets/grok-BuodbWaG.svg +31 -0
  1170. package/dist/ui/assets/index-BzJlVnNS.css +1 -0
  1171. package/dist/ui/assets/index-Cx7vZJHL.js +7 -0
  1172. package/dist/ui/assets/{vendor-dnd-DRzYolkg.js → vendor-dnd-CJ-AjP-Y.js} +1 -1
  1173. package/dist/ui/assets/{vendor-icons-CLnehDTw.js → vendor-icons-B8ZNsH1g.js} +1 -1
  1174. package/dist/ui/assets/{vendor-markdown-BUxTU7dS.js → vendor-markdown-_lhNCyq-.js} +9 -9
  1175. package/dist/ui/assets/vendor-router-AqJMU8Lz.js +3 -0
  1176. package/dist/ui/assets/vscode-C_7wk1WI.svg +41 -0
  1177. package/dist/ui/branding/logos/logo_dark_bg.png +0 -0
  1178. package/dist/ui/branding/logos/logo_favicon_dark.png +0 -0
  1179. package/dist/ui/branding/logos/logo_favicon_dark.svg +56 -0
  1180. package/dist/ui/branding/logos/logo_favicon_light.png +0 -0
  1181. package/dist/ui/branding/logos/logo_light_bg.png +0 -0
  1182. package/dist/ui/images/Taskforce_Van.webp +0 -0
  1183. package/dist/ui/index.html +6 -6
  1184. package/dist/update/uiUpdateService.d.ts +43 -0
  1185. package/dist/update/uiUpdateService.js +149 -0
  1186. package/dist/update/updateCli.d.ts +15 -0
  1187. package/dist/update/updateCli.js +76 -0
  1188. package/dist/update/updateExecutor.d.ts +40 -0
  1189. package/dist/update/updateExecutor.js +474 -0
  1190. package/dist/update/updateStatus.d.ts +83 -0
  1191. package/dist/update/updateStatus.js +703 -0
  1192. package/dist/utils/aiProfileAvatarPaths.d.ts +14 -0
  1193. package/dist/utils/aiProfileAvatarPaths.js +52 -0
  1194. package/dist/utils/aiProfileDefaultLogo.d.ts +7 -0
  1195. package/dist/utils/aiProfileDefaultLogo.js +84 -0
  1196. package/dist/utils/aiProfileEvents.d.ts +9 -1
  1197. package/dist/utils/annotatedAttachments.d.ts +8 -0
  1198. package/dist/utils/annotatedAttachments.js +17 -1
  1199. package/dist/utils/assetTraySorting.d.ts +25 -0
  1200. package/dist/utils/assetTraySorting.js +83 -0
  1201. package/dist/utils/assignees.d.ts +5 -0
  1202. package/dist/utils/assignees.js +14 -9
  1203. package/dist/utils/authLogin.d.ts +10 -0
  1204. package/dist/utils/authLogin.js +8 -0
  1205. package/dist/utils/authSessionEvents.d.ts +2 -0
  1206. package/dist/utils/authSessionEvents.js +6 -0
  1207. package/dist/utils/avatarUpload.js +5 -5
  1208. package/dist/utils/canonicalReferenceAttachments.d.ts +12 -0
  1209. package/dist/utils/canonicalReferenceAttachments.js +42 -0
  1210. package/dist/utils/cloudAssetDisplay.d.ts +2 -0
  1211. package/dist/utils/cloudAssetDisplay.js +8 -0
  1212. package/dist/utils/constants.d.ts +4 -11
  1213. package/dist/utils/constants.js +5 -64
  1214. package/dist/utils/contextAttachmentPreview.d.ts +11 -0
  1215. package/dist/utils/contextAttachmentPreview.js +8 -0
  1216. package/dist/utils/contextAttachmentUpload.d.ts +8 -0
  1217. package/dist/utils/contextAttachmentUpload.js +144 -0
  1218. package/dist/utils/documentNames.js +6 -4
  1219. package/dist/utils/entityReferences.d.ts +14 -0
  1220. package/dist/utils/entityReferences.js +21 -0
  1221. package/dist/utils/httpSetCookie.d.ts +3 -0
  1222. package/dist/utils/httpSetCookie.js +22 -0
  1223. package/dist/utils/imageNoteStoragePaths.d.ts +3 -0
  1224. package/dist/utils/imageNoteStoragePaths.js +19 -0
  1225. package/dist/utils/imageReferences.d.ts +6 -0
  1226. package/dist/utils/imageReferences.js +1 -0
  1227. package/dist/utils/planningArchiveOrchestrator.d.ts +23 -0
  1228. package/dist/utils/planningArchiveOrchestrator.js +37 -0
  1229. package/dist/utils/planningArchivePresentation.d.ts +20 -0
  1230. package/dist/utils/planningArchivePresentation.js +23 -0
  1231. package/dist/utils/planningArchiveReadiness.d.ts +26 -0
  1232. package/dist/utils/planningArchiveReadiness.js +30 -0
  1233. package/dist/utils/planningFavorites.d.ts +7 -0
  1234. package/dist/utils/planningFavorites.js +34 -0
  1235. package/dist/utils/syncEventPresentation.js +53 -0
  1236. package/dist/utils/syncStatusPresentation.d.ts +25 -0
  1237. package/dist/utils/syncStatusPresentation.js +86 -5
  1238. package/dist/utils/taskActivity.d.ts +9 -2
  1239. package/dist/utils/taskActivity.js +61 -0
  1240. package/dist/utils/taskDraftDefaults.d.ts +0 -8
  1241. package/dist/utils/taskDraftDefaults.js +0 -4
  1242. package/dist/utils/taskEvents.d.ts +11 -0
  1243. package/dist/utils/taskEvents.js +6 -0
  1244. package/dist/utils/taskHeaderQuickFilter.d.ts +14 -0
  1245. package/dist/utils/taskHeaderQuickFilter.js +32 -0
  1246. package/dist/utils/taskInvalidation.d.ts +23 -0
  1247. package/dist/utils/taskInvalidation.js +67 -0
  1248. package/dist/utils/taskNormalization.d.ts +2 -1
  1249. package/dist/utils/taskNormalization.js +44 -18
  1250. package/dist/utils/taskReferences.d.ts +16 -0
  1251. package/dist/utils/taskReferences.js +63 -0
  1252. package/dist/utils/taskforceAgentEvents.d.ts +8 -0
  1253. package/dist/utils/taskforceAgentEvents.js +6 -0
  1254. package/dist/utils/taskforceAgentSkillEvents.d.ts +7 -0
  1255. package/dist/utils/taskforceAgentSkillEvents.js +8 -0
  1256. package/dist/utils/theme.js +1 -1
  1257. package/dist/utils/uiNotice.d.ts +3 -0
  1258. package/dist/utils/uiNotice.js +1 -1
  1259. package/dist/utils/workspaceResourceEvents.d.ts +7 -0
  1260. package/dist/utils/workspaceResourceEvents.js +6 -0
  1261. package/dist/utils/workspaceSyncPresentation.d.ts +24 -0
  1262. package/dist/utils/workspaceSyncPresentation.js +243 -6
  1263. package/package.json +69 -21
  1264. package/dist/components/features/AgentsModule.d.ts +0 -18
  1265. package/dist/components/features/AgentsModule.js +0 -479
  1266. package/dist/components/features/InitiativesModule.d.ts +0 -22
  1267. package/dist/components/features/InitiativesModule.js +0 -33
  1268. package/dist/components/features/WorkflowsModule.d.ts +0 -27
  1269. package/dist/components/features/WorkflowsModule.js +0 -21
  1270. package/dist/components/features/settings/WorkflowEditor.d.ts +0 -15
  1271. package/dist/components/features/settings/WorkflowEditor.js +0 -245
  1272. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +0 -10
  1273. package/dist/components/views/panels/DocumentFilterToolbar.js +0 -7
  1274. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +0 -61
  1275. package/dist/components/views/standalone/modals/AccountHubModal.js +0 -108
  1276. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +0 -19
  1277. package/dist/components/views/standalone/modals/EditProfileModal.js +0 -11
  1278. package/dist/hooks/ui/useResourceExport.d.ts +0 -21
  1279. package/dist/hooks/ui/useResourceExport.js +0 -87
  1280. package/dist/hooks/ui/useWorkflowTemplates.d.ts +0 -37
  1281. package/dist/hooks/ui/useWorkflowTemplates.js +0 -116
  1282. package/dist/mcp/clientProfileDefaults.d.ts +0 -12
  1283. package/dist/mcp/clientProfileDefaults.js +0 -37
  1284. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +0 -55
  1285. package/dist/resources/templates/environments/aider.yaml +0 -14
  1286. package/dist/resources/templates/environments/antigravity.yaml +0 -10
  1287. package/dist/resources/templates/environments/claude.yaml +0 -10
  1288. package/dist/resources/templates/environments/cline.yaml +0 -10
  1289. package/dist/resources/templates/environments/codex.yaml +0 -13
  1290. package/dist/resources/templates/environments/copilot.yaml +0 -10
  1291. package/dist/resources/templates/environments/cursor.yaml +0 -9
  1292. package/dist/resources/templates/environments/gemini.yaml +0 -9
  1293. package/dist/resources/templates/environments/windsurf.yaml +0 -10
  1294. package/dist/resources/templates/workflow-sources/README.md +0 -9
  1295. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +0 -530
  1296. package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +0 -1
  1297. package/dist/resources/templates/workflows/collaborate.yaml +0 -110
  1298. package/dist/resources/templates/workflows/do.yaml +0 -88
  1299. package/dist/resources/templates/workflows/evaluate.yaml +0 -117
  1300. package/dist/resources/templates/workflows/execute.yaml +0 -159
  1301. package/dist/resources/templates/workflows/plan.yaml +0 -119
  1302. package/dist/resources/templates/workflows/review.yaml +0 -115
  1303. package/dist/services/formatTransformers.d.ts +0 -29
  1304. package/dist/services/formatTransformers.js +0 -56
  1305. package/dist/services/templateEngine.d.ts +0 -46
  1306. package/dist/services/templateEngine.js +0 -148
  1307. package/dist/shared/workflowDefaults.d.ts +0 -9
  1308. package/dist/shared/workflowDefaults.js +0 -11
  1309. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  1310. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  1311. package/dist/ui/agent-logos/chatgpt.png +0 -0
  1312. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  1313. package/dist/ui/agent-logos/codex.jpeg +0 -0
  1314. package/dist/ui/agent-logos/cursor.png +0 -0
  1315. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  1316. package/dist/ui/agent-logos/windsurf.png +0 -0
  1317. package/dist/ui/assets/AgentsModule-DvkbUHi0.js +0 -1
  1318. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +0 -1
  1319. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jhOhxqHT.js +0 -3
  1320. package/dist/ui/assets/ContextAttachmentManager-BAmEyLHL.js +0 -3
  1321. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +0 -1
  1322. package/dist/ui/assets/DocumentWorkspace-X-CpD5OO.js +0 -252
  1323. package/dist/ui/assets/EntityActivityTimeline-DD33onAC.js +0 -1
  1324. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +0 -1
  1325. package/dist/ui/assets/InitiativesModule-5nDXtIjf.js +0 -1
  1326. package/dist/ui/assets/PlansPage-B8nDR1hQ.js +0 -1
  1327. package/dist/ui/assets/PlansPage-DeAzWYTb.css +0 -1
  1328. package/dist/ui/assets/TaskContextUpload-DrGrXJHm.js +0 -1
  1329. package/dist/ui/assets/TaskSettings-BXVvZZwH.js +0 -9
  1330. package/dist/ui/assets/WorkflowsModule-DWCGhEWT.js +0 -5
  1331. package/dist/ui/assets/documentReferences-Crup321p.js +0 -1
  1332. package/dist/ui/assets/index-B9k24Aym.js +0 -5
  1333. package/dist/ui/assets/index-DNeR5dYW.css +0 -1
  1334. package/dist/ui/assets/vendor-router-BbWMxlnO.js +0 -3
  1335. package/dist/ui/branding/logos/logo_black_01.png +0 -0
  1336. package/dist/ui/branding/logos/logo_black_02.png +0 -0
  1337. package/dist/ui/branding/logos/logo_white_01.png +0 -0
  1338. package/dist/ui/branding/logos/logo_white_02.png +0 -0
  1339. package/scripts/generate-workflow-templates.mjs +0 -134
  1340. package/src/resources/templates/WORKFLOW_V2_POLICY.md +0 -55
  1341. package/src/resources/templates/environments/aider.yaml +0 -14
  1342. package/src/resources/templates/environments/antigravity.yaml +0 -10
  1343. package/src/resources/templates/environments/claude.yaml +0 -10
  1344. package/src/resources/templates/environments/cline.yaml +0 -10
  1345. package/src/resources/templates/environments/codex.yaml +0 -13
  1346. package/src/resources/templates/environments/copilot.yaml +0 -10
  1347. package/src/resources/templates/environments/cursor.yaml +0 -9
  1348. package/src/resources/templates/environments/gemini.yaml +0 -9
  1349. package/src/resources/templates/environments/windsurf.yaml +0 -10
  1350. package/src/resources/templates/workflow-sources/README.md +0 -9
  1351. package/src/resources/templates/workflow-sources/workflowDocs.mjs +0 -530
  1352. package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +0 -1
  1353. package/src/resources/templates/workflows/collaborate.yaml +0 -110
  1354. package/src/resources/templates/workflows/do.yaml +0 -88
  1355. package/src/resources/templates/workflows/evaluate.yaml +0 -117
  1356. package/src/resources/templates/workflows/execute.yaml +0 -159
  1357. package/src/resources/templates/workflows/plan.yaml +0 -119
  1358. package/src/resources/templates/workflows/review.yaml +0 -115
  1359. /package/dist/ui/branding/logos/{logo_dark.svg → logo_dark_no_bg.svg} +0 -0
  1360. /package/dist/ui/branding/logos/{logo_favicon.svg → logo_favicon_light.svg} +0 -0
  1361. /package/dist/ui/branding/logos/{logo_light.svg → logo_light_no_bg.svg} +0 -0
@@ -0,0 +1,4635 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { normalizePlanningComments, } from '../planningCanonicalFields.js';
3
+ import { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
4
+ import { buildTaskCreateActivityEventId } from './taskCreateActivityIdentity.js';
5
+ import { buildTaskCommentActivityEventId } from './taskCommentActivityIdentity.js';
6
+ import { buildTaskLifecycleActivityEventId, } from './taskLifecycleActivityIdentity.js';
7
+ import { buildTaskMetadataActivityEventId } from './taskMetadataActivityIdentity.js';
8
+ import { parseWorkspaceSyncV3OriginActor } from './workspaceSyncV3OriginActor.js';
9
+ import { WorkspaceSyncJournalReader } from './workspaceSyncJournalReader.js';
10
+ import { isLegacyEntityEventAdoptionIdentity, LEGACY_ENTITY_EVENT_ADOPTION_MAX_PAYLOAD_BYTES, } from './entityEventLegacyAdoptionContract.js';
11
+ import { validateWorkspaceSyncV3EntityEventPayload } from './workspaceSyncV3NestedTaskCodecs.js';
12
+ import { fingerprintWorkspaceSyncPayload } from './workspaceSyncPayloadFingerprint.js';
13
+ export { canonicalWorkspaceSyncJson } from './canonicalWorkspaceSyncJson.js';
14
+ export { fingerprintWorkspaceSyncPayload } from './workspaceSyncPayloadFingerprint.js';
15
+ export const WORKSPACE_SYNC_CONTRACT_VERSION = 3;
16
+ export const WORKSPACE_SYNC_INITIATIVE_PROJECTION_LOCK = 'projection:initiative/full-v1';
17
+ export const WORKSPACE_SYNC_COMBINED_PROJECTION = 'coverage/task-planning-metadata-v1';
18
+ export const WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK = 'workspace-sync-v3-combined-projection';
19
+ const MAX_OUTBOX_FUTURE_DEADLINE_MS = 48 * 60 * 60_000;
20
+ const TASK_AGGREGATE_SUCCESSOR_MUTATIONS = new Map([
21
+ ['task-comments', new Set(['append', 'reconcile'])],
22
+ ['task-checklist', new Set(['replace'])],
23
+ ['task-attachment-links', new Set(['replace'])],
24
+ ]);
25
+ const TASK_ROOT_SUCCESSOR_MUTATIONS = new Set([
26
+ 'metadata-update', 'set-status', 'archive', 'unarchive', 'complete', 'reopen',
27
+ ]);
28
+ const TASK_CREATE_RECOVERY_ERROR_CODES = new Set([
29
+ 'TASK_CREATE_ID_CONFLICT',
30
+ 'WORKSPACE_SYNC_REVISION_CONFLICT',
31
+ ]);
32
+ const TASK_CREATE_FIELDS = new Set([
33
+ 'title', 'description', 'priority', 'complexity', 'type', 'category', 'assignee',
34
+ 'status', 'scheduledDate', 'dueDate', 'orderInDay', 'workstreamId', 'taxonomies',
35
+ ]);
36
+ const TASK_ATTACHMENT_LINK_FIELDS = new Set([
37
+ 'assetId', 'role', 'order', 'caption', 'displayName', 'originalFilename',
38
+ 'referenceNumber', 'referenceLabel',
39
+ ]);
40
+ const TASK_ATTACHMENT_FORBIDDEN_METADATA_VALUE = /^(?:file|data|blob):|^https?:\/\/|^(?:~(?:\/|$)|\/(?:Users|home|private|tmp|var)(?:\/|$))|^[A-Za-z]:[\\/]/i;
41
+ const TASK_LIFECYCLE_ACTIVITY_MUTATIONS = new Set([
42
+ 'archive', 'unarchive', 'complete', 'reopen', 'set-status', 'restore',
43
+ ]);
44
+ function compareCodeUnits(left, right) {
45
+ return left < right ? -1 : left > right ? 1 : 0;
46
+ }
47
+ function normalizeWorkspaceSyncLockTargets(targets) {
48
+ return Array.from(new Map(targets.map((target) => {
49
+ const domain = requireText(target.domain, 'Domain');
50
+ const entityId = requireText(target.entityId, 'Entity id');
51
+ return [`${domain}\u0000${entityId}`, { domain, entityId }];
52
+ })).values()).sort((left, right) => (compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.entityId, right.entityId)));
53
+ }
54
+ function requiresInitiativeProjectionLock(targets) {
55
+ return targets.some((target) => (target.domain === 'initiative'
56
+ || target.domain === 'initiative-assets'
57
+ || target.domain === 'initiative-owners'));
58
+ }
59
+ function requiresCombinedProjectionLock(targets) {
60
+ return targets.some((target) => (target.domain === 'task'
61
+ || target.domain === 'task-relationship'
62
+ || target.domain === 'taxonomy'
63
+ || target.domain === 'initiative'
64
+ || target.domain === 'workstream'));
65
+ }
66
+ export class WorkspaceSyncIdempotencyConflictError extends Error {
67
+ code = 'WORKSPACE_SYNC_IDEMPOTENCY_CONFLICT';
68
+ statusCode = 409;
69
+ constructor() {
70
+ super('The operation id is already bound to a different request.');
71
+ this.name = 'WorkspaceSyncIdempotencyConflictError';
72
+ }
73
+ }
74
+ export class WorkspaceSyncPlanningOwnershipChangedError extends Error {
75
+ code = 'WORKSPACE_SYNC_PLANNING_OWNERSHIP_CHANGED';
76
+ statusCode = 409;
77
+ constructor() {
78
+ super('Durable planning ownership changed before the local operation could be captured.');
79
+ this.name = 'WorkspaceSyncPlanningOwnershipChangedError';
80
+ }
81
+ }
82
+ export class WorkspaceSyncLeaseLostError extends Error {
83
+ code = 'WORKSPACE_SYNC_LEASE_LOST';
84
+ constructor() {
85
+ super('The workspace sync operation lease is no longer owned by this caller.');
86
+ this.name = 'WorkspaceSyncLeaseLostError';
87
+ }
88
+ }
89
+ export class WorkspaceSyncRevisionConflictError extends Error {
90
+ code = 'WORKSPACE_SYNC_REVISION_CONFLICT';
91
+ statusCode = 409;
92
+ details;
93
+ constructor(details) {
94
+ super('The workspace entity revision no longer matches the operation base revision.');
95
+ this.name = 'WorkspaceSyncRevisionConflictError';
96
+ this.details = details;
97
+ }
98
+ }
99
+ export class WorkspaceSyncEntityBusyError extends Error {
100
+ code = 'WORKSPACE_SYNC_ENTITY_BUSY';
101
+ statusCode = 409;
102
+ details;
103
+ retryable;
104
+ retryAfterAt;
105
+ retryHint;
106
+ constructor(input) {
107
+ super('The workspace entity already has an unaccepted local operation.');
108
+ this.name = 'WorkspaceSyncEntityBusyError';
109
+ this.details = input?.details;
110
+ this.retryable = input?.retryable !== false;
111
+ this.retryAfterAt = input?.retryAfterAt;
112
+ this.retryHint = input?.retryHint || (this.retryable
113
+ ? this.retryAfterAt
114
+ ? `Wait until at least ${this.retryAfterAt} for Sync Manager to retry the blocking operation, then retry the same tool call. Refresh alone will not clear it.`
115
+ : 'Let Sync Manager dispatch the blocking operation, then retry the same tool call. Refresh alone will not clear it.'
116
+ : 'The blocking sync operation needs recovery. Do not keep retrying this mutation; open Sync Manager and resolve the reported blocked operation first.');
117
+ }
118
+ }
119
+ export function resolveWorkspaceSyncActiveOperationState(input) {
120
+ const status = String(input.status || '');
121
+ if (status === 'blocked')
122
+ return 'blocked';
123
+ const nowMs = Date.parse(input.now);
124
+ const nextAttemptMs = input.nextAttemptAt == null
125
+ ? Number.NaN
126
+ : Date.parse(String(input.nextAttemptAt));
127
+ const leaseExpiresMs = input.leaseExpiresAt == null
128
+ ? Number.NaN
129
+ : Date.parse(String(input.leaseExpiresAt));
130
+ if (status === 'in_flight') {
131
+ return Number.isFinite(leaseExpiresMs) && leaseExpiresMs <= nowMs
132
+ ? 'stale'
133
+ : 'in-flight';
134
+ }
135
+ return Number.isFinite(nextAttemptMs) && nextAttemptMs > nowMs
136
+ ? 'retry-scheduled'
137
+ : 'pending';
138
+ }
139
+ function workspaceSyncEntityBusyError(input) {
140
+ const nowMs = Date.parse(input.now);
141
+ const allBlockingOperations = input.active.map((row) => {
142
+ const status = String(row.status || '');
143
+ const nextAttemptAt = row.next_attempt_at == null ? null : String(row.next_attempt_at);
144
+ const leaseExpiresAt = row.lease_expires_at == null ? null : String(row.lease_expires_at);
145
+ const state = resolveWorkspaceSyncActiveOperationState({
146
+ status,
147
+ nextAttemptAt,
148
+ leaseExpiresAt,
149
+ now: input.now,
150
+ });
151
+ return {
152
+ operationId: String(row.operation_id || ''),
153
+ domain: String(row.domain || ''),
154
+ entityId: String(row.entity_id || ''),
155
+ mutationKind: String(row.mutation_kind || ''),
156
+ state,
157
+ attemptCount: Number(row.attempt_count || 0),
158
+ createdAt: String(row.created_at || ''),
159
+ updatedAt: String(row.updated_at || ''),
160
+ ...(nextAttemptAt ? { nextAttemptAt } : {}),
161
+ ...(leaseExpiresAt ? { leaseExpiresAt } : {}),
162
+ ...(row.last_error_code ? { errorCode: String(row.last_error_code) } : {}),
163
+ };
164
+ });
165
+ const blockingOperations = allBlockingOperations.slice(0, 5);
166
+ const retryable = allBlockingOperations.every((operation) => operation.state !== 'blocked');
167
+ const retryAfterAt = retryable
168
+ ? allBlockingOperations
169
+ .flatMap((operation) => [operation.nextAttemptAt, operation.leaseExpiresAt])
170
+ .filter((value) => typeof value === 'string' && Date.parse(value) > nowMs)
171
+ .sort((left, right) => Date.parse(right) - Date.parse(left))[0]
172
+ : undefined;
173
+ const retryHint = retryable && !retryAfterAt
174
+ && allBlockingOperations.some((operation) => operation.state === 'stale')
175
+ ? 'The blocking operation has an expired lease and is reclaimable. Run or check Sync Manager, then retry the same tool call after it reclaims the operation. Refresh alone will not clear it.'
176
+ : undefined;
177
+ return new WorkspaceSyncEntityBusyError({
178
+ retryable,
179
+ retryAfterAt,
180
+ retryHint,
181
+ details: {
182
+ target: input.target,
183
+ blockingOperations,
184
+ blockingOperationCount: input.active.length,
185
+ diagnosticsTruncated: input.active.length > blockingOperations.length,
186
+ },
187
+ });
188
+ }
189
+ function requireText(value, label) {
190
+ const normalized = String(value || '').trim();
191
+ if (!normalized)
192
+ throw new Error(`${label} is required.`);
193
+ return normalized;
194
+ }
195
+ function requireOutboxDispatchClass(value) {
196
+ const normalized = requireText(value, 'Outbox dispatch class');
197
+ if (normalized !== 'ordinary' && normalized !== 'legacy-adoption') {
198
+ throw new Error('Outbox dispatch class is invalid.');
199
+ }
200
+ return normalized;
201
+ }
202
+ function requireIsoTimestamp(value, label) {
203
+ const normalized = requireText(value, label);
204
+ const epochMs = Date.parse(normalized);
205
+ if (!Number.isFinite(epochMs))
206
+ throw new Error(`${label} must be an ISO timestamp.`);
207
+ return new Date(epochMs).toISOString();
208
+ }
209
+ /**
210
+ * Authoritative feed apply may replace a provisional entity event only after
211
+ * durable recovery has explicitly superseded the operation that created it.
212
+ * Persisted targets or a recognized deterministic task-root identity provide
213
+ * the provenance proof; arbitrary same-ID conflicts continue to fail closed.
214
+ */
215
+ export function hasCompletedRecoveryForSupersededEntityEvent(db, input) {
216
+ const tenantId = requireText(input.tenantId, 'Tenant id');
217
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
218
+ const entityType = requireText(input.entityType, 'Entity type');
219
+ const entityId = requireText(input.entityId, 'Entity id');
220
+ const eventId = requireText(input.eventId, 'Entity event id');
221
+ const explicitTarget = db.prepare(`
222
+ SELECT 1 AS proven
223
+ FROM workspace_sync_outbox AS source
224
+ INNER JOIN workspace_sync_outbox_recoveries AS recovery
225
+ ON recovery.tenant_id = source.tenant_id
226
+ AND recovery.workspace_id = source.workspace_id
227
+ AND recovery.recovery_batch_id = source.recovery_batch_id
228
+ INNER JOIN workspace_sync_outbox_targets AS event_target
229
+ ON event_target.tenant_id = source.tenant_id
230
+ AND event_target.workspace_id = source.workspace_id
231
+ AND event_target.client_id = source.client_id
232
+ AND event_target.operation_id = source.operation_id
233
+ AND event_target.domain = 'entity-event'
234
+ AND event_target.entity_id = ?
235
+ INNER JOIN workspace_sync_outbox_targets AS parent_target
236
+ ON parent_target.tenant_id = source.tenant_id
237
+ AND parent_target.workspace_id = source.workspace_id
238
+ AND parent_target.client_id = source.client_id
239
+ AND parent_target.operation_id = source.operation_id
240
+ AND parent_target.domain = ?
241
+ AND parent_target.entity_id = ?
242
+ WHERE source.tenant_id = ?
243
+ AND source.workspace_id = ?
244
+ AND source.status = 'superseded'
245
+ AND recovery.state = 'completed'
246
+ AND recovery.completed_at IS NOT NULL
247
+ LIMIT 1
248
+ `).get(eventId, entityType, entityId, tenantId, workspaceId);
249
+ if (Number(explicitTarget?.proven || 0) === 1)
250
+ return true;
251
+ if (entityType !== 'task')
252
+ return false;
253
+ const sources = db.prepare(`
254
+ SELECT source.client_id, source.operation_id, source.mutation_kind
255
+ FROM workspace_sync_outbox AS source
256
+ INNER JOIN workspace_sync_outbox_recoveries AS recovery
257
+ ON recovery.tenant_id = source.tenant_id
258
+ AND recovery.workspace_id = source.workspace_id
259
+ AND recovery.recovery_batch_id = source.recovery_batch_id
260
+ WHERE source.tenant_id = ?
261
+ AND source.workspace_id = ?
262
+ AND source.domain = 'task'
263
+ AND source.entity_id = ?
264
+ AND source.status = 'superseded'
265
+ AND recovery.state = 'completed'
266
+ AND recovery.completed_at IS NOT NULL
267
+ `).all(tenantId, workspaceId, entityId);
268
+ return sources.some((source) => {
269
+ const clientId = String(source.client_id || '').trim();
270
+ const operationId = String(source.operation_id || '').trim();
271
+ const mutationKind = String(source.mutation_kind || '').trim();
272
+ if (!clientId || !operationId)
273
+ return false;
274
+ if (mutationKind === 'create') {
275
+ return buildTaskCreateActivityEventId({
276
+ workspaceId, clientId, operationId, taskId: entityId,
277
+ }) === eventId;
278
+ }
279
+ if (mutationKind === 'metadata-update') {
280
+ return buildTaskMetadataActivityEventId({
281
+ workspaceId, clientId, operationId, taskId: entityId,
282
+ }) === eventId;
283
+ }
284
+ if (TASK_LIFECYCLE_ACTIVITY_MUTATIONS.has(mutationKind)) {
285
+ return buildTaskLifecycleActivityEventId({
286
+ workspaceId,
287
+ clientId,
288
+ operationId,
289
+ taskId: entityId,
290
+ mutationKind: mutationKind,
291
+ }) === eventId;
292
+ }
293
+ return false;
294
+ });
295
+ }
296
+ function buildRecoveryReplayOperationId(recoveryBatchId, baselineMarker, clientId, operationId) {
297
+ const digest = createHash('sha256')
298
+ .update(`${recoveryBatchId}\0${baselineMarker}\0${clientId}\0${operationId}`, 'utf8')
299
+ .digest('hex');
300
+ return `recovery-${digest}`;
301
+ }
302
+ function buildEntityEventAdoptionRecoveryReplayOperationId(recoveryBatchId, baselineMarker, clientId, operationId) {
303
+ const digest = createHash('sha256')
304
+ .update(`${recoveryBatchId}\0${baselineMarker}\0${clientId}\0${operationId}`, 'utf8')
305
+ .digest('hex');
306
+ return `legacy-entity-event-adoption-${digest}`;
307
+ }
308
+ function normalizeSequence(value) {
309
+ if (typeof value === 'number' && !Number.isSafeInteger(value)) {
310
+ throw new Error('Workspace sync sequence exceeded the JavaScript safe integer range.');
311
+ }
312
+ const normalized = String(value ?? '').trim();
313
+ if (!/^\d+$/.test(normalized))
314
+ throw new Error('Workspace sync sequence is invalid.');
315
+ return BigInt(normalized).toString(10);
316
+ }
317
+ function normalizeRevisionKind(value) {
318
+ if (value === 'metadata' || value === 'lifecycle' || value === 'both')
319
+ return value;
320
+ throw new Error('Revision kind must be metadata, lifecycle, or both.');
321
+ }
322
+ function operationParams(identity) {
323
+ return [
324
+ requireText(identity.tenantId, 'Tenant id'),
325
+ requireText(identity.workspaceId, 'Workspace id'),
326
+ requireText(identity.clientId, 'Client id'),
327
+ requireText(identity.operationId, 'Operation id'),
328
+ ];
329
+ }
330
+ export class WorkspaceSyncDurabilityStore {
331
+ db;
332
+ journalReader;
333
+ constructor(db) {
334
+ this.db = db;
335
+ this.journalReader = new WorkspaceSyncJournalReader(db);
336
+ }
337
+ readEntityRevisionCoordinates(input) {
338
+ const tenantId = requireText(input.tenantId, 'Tenant id');
339
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
340
+ const domain = requireText(input.domain, 'Domain');
341
+ const entityId = requireText(input.entityId, 'Entity id');
342
+ return this.journalReader.readEntityRevisionCoordinates({ tenantId, workspaceId, domain, entityId });
343
+ }
344
+ readInitiativeProjectionOwnership(input) {
345
+ const tenantId = requireText(input.tenantId, 'Tenant id');
346
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
347
+ const rows = this.db.prepare(`
348
+ SELECT projection_id, owner_mode, CAST(epoch AS TEXT) AS epoch
349
+ FROM workspace_sync_projection_state
350
+ WHERE tenant_id = ? AND workspace_id = ?
351
+ AND projection_id IN (?, 'initiative/full-v1')
352
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION);
353
+ let combined = null;
354
+ let initiative = null;
355
+ for (const row of rows) {
356
+ if ((row.owner_mode !== 'v2' && row.owner_mode !== 'v3')
357
+ || !/^\d+$/.test(String(row.epoch ?? ''))) {
358
+ throw new Error('Workspace sync initiative projection ownership is invalid.');
359
+ }
360
+ const ownership = {
361
+ owner: row.owner_mode,
362
+ epoch: BigInt(String(row.epoch)).toString(10),
363
+ };
364
+ if (row.projection_id === WORKSPACE_SYNC_COMBINED_PROJECTION)
365
+ combined = ownership;
366
+ else if (row.projection_id === 'initiative/full-v1')
367
+ initiative = ownership;
368
+ else
369
+ throw new Error('Workspace sync initiative projection ownership is invalid.');
370
+ }
371
+ if (combined?.owner === 'v3')
372
+ return combined;
373
+ if (initiative)
374
+ return initiative;
375
+ if (combined) {
376
+ if (combined.owner !== 'v2') {
377
+ throw new Error('Workspace sync combined projection ownership is invalid.');
378
+ }
379
+ return { owner: 'v2', epoch: '0' };
380
+ }
381
+ return { owner: 'v2', epoch: '0' };
382
+ }
383
+ readCombinedProjectionOwnership(input) {
384
+ const tenantId = requireText(input.tenantId, 'Tenant id');
385
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
386
+ const row = this.db.prepare(`
387
+ SELECT owner_mode, CAST(epoch AS TEXT) AS epoch
388
+ FROM workspace_sync_projection_state
389
+ WHERE tenant_id = ? AND workspace_id = ? AND projection_id = ?
390
+ `).get(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION);
391
+ if (!row)
392
+ return { owner: 'v2', epoch: '0' };
393
+ if ((row.owner_mode !== 'v2' && row.owner_mode !== 'v3')
394
+ || !/^\d+$/.test(String(row.epoch ?? ''))) {
395
+ throw new Error('Workspace sync combined projection ownership is invalid.');
396
+ }
397
+ return {
398
+ owner: row.owner_mode,
399
+ epoch: BigInt(String(row.epoch)).toString(10),
400
+ };
401
+ }
402
+ runCombinedProjectionTransaction(input) {
403
+ const tenantId = requireText(input.tenantId, 'Tenant id');
404
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
405
+ const now = requireIsoTimestamp(input.now, 'Combined projection transaction time');
406
+ return this.db.transaction(() => {
407
+ this.db.prepare(`
408
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
409
+ VALUES (?, ?, ?, ?)
410
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
411
+ `).run(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
412
+ return input.apply(this.readCombinedProjectionOwnership({ tenantId, workspaceId }));
413
+ })();
414
+ }
415
+ readWorkspaceDomainOwnership(input) {
416
+ const tenantId = requireText(input.tenantId, 'Tenant id');
417
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
418
+ const domain = requireText(input.domain, 'Domain');
419
+ const entityId = requireText(input.entityId, 'Entity id');
420
+ const revision = this.db.prepare(`
421
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
422
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision,
423
+ CAST(last_sequence AS TEXT) AS last_sequence,
424
+ canonical_fingerprint, coexistence_owned
425
+ FROM workspace_sync_entity_revisions
426
+ WHERE tenant_id = ? AND workspace_id = ? AND contract_version = ?
427
+ AND domain = ? AND entity_id = ?
428
+ `).get(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, domain, entityId);
429
+ const active = this.db.prepare(`
430
+ SELECT 1 AS active
431
+ FROM workspace_sync_outbox AS outbox
432
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
433
+ AND outbox.contract_version = ?
434
+ AND outbox.status IN ('pending', 'in_flight', 'blocked')
435
+ AND (
436
+ (outbox.domain = ? AND outbox.entity_id = ?)
437
+ OR EXISTS (
438
+ SELECT 1 FROM workspace_sync_outbox_targets AS target
439
+ WHERE target.tenant_id = outbox.tenant_id
440
+ AND target.workspace_id = outbox.workspace_id
441
+ AND target.client_id = outbox.client_id
442
+ AND target.operation_id = outbox.operation_id
443
+ AND target.domain = ? AND target.entity_id = ?
444
+ )
445
+ )
446
+ LIMIT 1
447
+ `).get(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, domain, entityId, domain, entityId);
448
+ const entityRevision = normalizeSequence(revision?.entity_revision ?? 0);
449
+ return {
450
+ domain,
451
+ entityId,
452
+ activeOperation: Number(active?.active || 0) === 1,
453
+ owned: BigInt(entityRevision) > 0n && Number(revision?.coexistence_owned) === 1,
454
+ entityRevision,
455
+ lifecycleRevision: normalizeSequence(revision?.lifecycle_revision ?? 0),
456
+ lastSequence: revision?.last_sequence === undefined ? null : normalizeSequence(revision.last_sequence),
457
+ canonicalFingerprint: revision?.canonical_fingerprint
458
+ ? requireText(revision.canonical_fingerprint, 'Canonical fingerprint')
459
+ : null,
460
+ };
461
+ }
462
+ listWorkspaceDomainOwnership(input) {
463
+ const tenantId = requireText(input.tenantId, 'Tenant id');
464
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
465
+ const domain = requireText(input.domain, 'Domain');
466
+ const entityIds = Array.from(new Set(input.entityIds
467
+ .map((entityId) => requireText(entityId, 'Entity id'))))
468
+ .sort();
469
+ if (entityIds.length === 0)
470
+ return [];
471
+ if (entityIds.length > 100)
472
+ throw new Error('Domain ownership lookup supports at most 100 entity ids.');
473
+ const values = entityIds.map(() => '(?)').join(', ');
474
+ const rows = this.db.prepare(`
475
+ WITH requested(entity_id) AS (VALUES ${values})
476
+ SELECT requested.entity_id,
477
+ CAST(revision.entity_revision AS TEXT) AS entity_revision,
478
+ CAST(revision.lifecycle_revision AS TEXT) AS lifecycle_revision,
479
+ CAST(revision.last_sequence AS TEXT) AS last_sequence,
480
+ revision.canonical_fingerprint,
481
+ revision.coexistence_owned,
482
+ CASE WHEN EXISTS (
483
+ SELECT 1
484
+ FROM workspace_sync_outbox AS outbox
485
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
486
+ AND outbox.contract_version = ?
487
+ AND outbox.status IN ('pending', 'in_flight', 'blocked')
488
+ AND (
489
+ (outbox.domain = ? AND outbox.entity_id = requested.entity_id)
490
+ OR EXISTS (
491
+ SELECT 1 FROM workspace_sync_outbox_targets AS target
492
+ WHERE target.tenant_id = outbox.tenant_id
493
+ AND target.workspace_id = outbox.workspace_id
494
+ AND target.client_id = outbox.client_id
495
+ AND target.operation_id = outbox.operation_id
496
+ AND target.domain = ? AND target.entity_id = requested.entity_id
497
+ )
498
+ )
499
+ ) THEN 1 ELSE 0 END AS active_operation
500
+ FROM requested
501
+ LEFT JOIN workspace_sync_entity_revisions AS revision
502
+ ON revision.tenant_id = ? AND revision.workspace_id = ?
503
+ AND revision.contract_version = ? AND revision.domain = ?
504
+ AND revision.entity_id = requested.entity_id
505
+ ORDER BY requested.entity_id
506
+ `).all(...entityIds, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, domain, domain, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, domain);
507
+ return rows.map((row) => {
508
+ const entityRevision = normalizeSequence(row.entity_revision ?? 0);
509
+ return {
510
+ domain,
511
+ entityId: requireText(row.entity_id, 'Entity id'),
512
+ activeOperation: Number(row.active_operation || 0) === 1,
513
+ owned: BigInt(entityRevision) > 0n && Number(row.coexistence_owned) === 1,
514
+ entityRevision,
515
+ lifecycleRevision: normalizeSequence(row.lifecycle_revision ?? 0),
516
+ lastSequence: row.last_sequence === undefined || row.last_sequence === null
517
+ ? null
518
+ : normalizeSequence(row.last_sequence),
519
+ canonicalFingerprint: row.canonical_fingerprint
520
+ ? requireText(row.canonical_fingerprint, 'Canonical fingerprint')
521
+ : null,
522
+ };
523
+ });
524
+ }
525
+ listOwnedWorkspaceDomainRevisionsAfter(input) {
526
+ const tenantId = requireText(input.tenantId, 'Tenant id');
527
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
528
+ const domains = Array.from(new Set(input.domains.map((domain) => requireText(domain, 'Domain')))).sort();
529
+ const sequence = normalizeSequence(input.sequence);
530
+ if (domains.length === 0)
531
+ return [];
532
+ const placeholders = domains.map(() => '?').join(', ');
533
+ const rows = this.db.prepare(`
534
+ SELECT domain, entity_id, CAST(last_sequence AS TEXT) AS last_sequence
535
+ FROM workspace_sync_entity_revisions
536
+ WHERE tenant_id = ? AND workspace_id = ? AND contract_version = ?
537
+ AND domain IN (${placeholders})
538
+ AND coexistence_owned = 1
539
+ AND entity_revision > 0
540
+ AND last_sequence IS NOT NULL
541
+ AND last_sequence > ?
542
+ ORDER BY domain ASC, entity_id ASC
543
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, ...domains, sequence);
544
+ return rows.map((row) => ({
545
+ domain: requireText(row.domain, 'Domain'),
546
+ entityId: requireText(row.entity_id, 'Entity id'),
547
+ lastSequence: normalizeSequence(row.last_sequence),
548
+ }));
549
+ }
550
+ applyReplicatedEntityRevision(input) {
551
+ const tenantId = requireText(input.tenantId, 'Tenant id');
552
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
553
+ const domain = requireText(input.domain, 'Domain');
554
+ const entityId = requireText(input.entityId, 'Entity id');
555
+ const sequence = normalizeSequence(input.sequence);
556
+ const entityRevision = normalizeSequence(input.entityRevision);
557
+ const lifecycleRevision = normalizeSequence(input.lifecycleRevision);
558
+ const canonicalFingerprint = requireText(input.canonicalFingerprint, 'Canonical fingerprint');
559
+ const now = requireIsoTimestamp(input.now, 'Replicated revision timestamp');
560
+ if (BigInt(sequence) <= 0n || BigInt(entityRevision) <= 0n || !/^[a-f0-9]{64}$/.test(canonicalFingerprint)) {
561
+ throw new WorkspaceSyncRevisionConflictError({
562
+ domain,
563
+ entityId,
564
+ reason: 'invalid-replicated-revision',
565
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
566
+ });
567
+ }
568
+ return this.db.transaction(() => {
569
+ const current = this.readWorkspaceDomainOwnership({ tenantId, workspaceId, domain, entityId });
570
+ const applyAuthoritativeRevision = () => {
571
+ const result = input.applyMutation();
572
+ if (input.shouldRecordRevision?.(result) === false) {
573
+ return { state: 'deferred', result };
574
+ }
575
+ this.db.prepare(`
576
+ INSERT INTO workspace_sync_entity_revisions (
577
+ tenant_id, workspace_id, domain, entity_id, contract_version,
578
+ entity_revision, lifecycle_revision, coexistence_owned, metadata_owned, reopen_assignee_owned,
579
+ last_sequence, canonical_fingerprint, updated_at
580
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
581
+ ON CONFLICT (tenant_id, workspace_id, domain, entity_id) DO UPDATE SET
582
+ contract_version = excluded.contract_version,
583
+ entity_revision = excluded.entity_revision,
584
+ lifecycle_revision = excluded.lifecycle_revision,
585
+ coexistence_owned = 1,
586
+ metadata_owned = CASE
587
+ WHEN workspace_sync_entity_revisions.metadata_owned = 1 OR excluded.metadata_owned = 1 THEN 1
588
+ ELSE 0
589
+ END,
590
+ reopen_assignee_owned = CASE
591
+ WHEN workspace_sync_entity_revisions.reopen_assignee_owned = 1
592
+ OR excluded.reopen_assignee_owned = 1 THEN 1
593
+ ELSE 0
594
+ END,
595
+ last_sequence = excluded.last_sequence,
596
+ canonical_fingerprint = excluded.canonical_fingerprint,
597
+ updated_at = excluded.updated_at
598
+ `).run(tenantId, workspaceId, domain, entityId, WORKSPACE_SYNC_CONTRACT_VERSION, entityRevision, lifecycleRevision, input.metadataOwned ? 1 : 0, input.reopenAssigneeOwned ? 1 : 0, sequence, canonicalFingerprint, now);
599
+ return { state: 'applied', result };
600
+ };
601
+ const incomingEntity = BigInt(entityRevision);
602
+ const currentEntity = BigInt(current.entityRevision);
603
+ const incomingLifecycle = BigInt(lifecycleRevision);
604
+ const currentLifecycle = BigInt(current.lifecycleRevision);
605
+ if (input.allowAuthoritativeRevisionReset
606
+ && (incomingEntity < currentEntity || incomingLifecycle < currentLifecycle)) {
607
+ const reset = this.db.prepare(`
608
+ UPDATE workspace_sync_entity_revisions
609
+ SET entity_revision = ?, lifecycle_revision = ?, last_sequence = ?, updated_at = ?
610
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
611
+ AND entity_revision = ? AND lifecycle_revision = ?
612
+ `).run(entityRevision, lifecycleRevision, sequence, now, tenantId, workspaceId, domain, entityId, current.entityRevision, current.lifecycleRevision);
613
+ if (Number(reset.changes || 0) !== 1) {
614
+ throw new WorkspaceSyncRevisionConflictError({
615
+ domain,
616
+ entityId,
617
+ reason: 'authoritative-recovery-revision-reset-raced',
618
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
619
+ current,
620
+ });
621
+ }
622
+ return { state: 'current' };
623
+ }
624
+ if (incomingEntity < currentEntity)
625
+ return { state: 'stale' };
626
+ if (incomingLifecycle < currentLifecycle) {
627
+ throw new WorkspaceSyncRevisionConflictError({
628
+ domain,
629
+ entityId,
630
+ reason: 'lifecycle-revision-regression',
631
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
632
+ current,
633
+ });
634
+ }
635
+ if (incomingEntity === currentEntity) {
636
+ const coordinatesMatch = current.lifecycleRevision === lifecycleRevision
637
+ && current.lastSequence === sequence;
638
+ if (!coordinatesMatch)
639
+ throw new WorkspaceSyncRevisionConflictError({
640
+ domain,
641
+ entityId,
642
+ reason: 'equal-revision-divergence',
643
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
644
+ current,
645
+ });
646
+ if (current.canonicalFingerprint !== canonicalFingerprint) {
647
+ if (!input.allowAuthoritativeEqualRevisionReplacement) {
648
+ throw new WorkspaceSyncRevisionConflictError({
649
+ domain,
650
+ entityId,
651
+ reason: 'equal-revision-divergence',
652
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
653
+ current,
654
+ });
655
+ }
656
+ const result = input.applyMutation();
657
+ if (input.shouldRecordRevision?.(result) === false) {
658
+ return { state: 'deferred', result };
659
+ }
660
+ const updated = this.db.prepare(`
661
+ UPDATE workspace_sync_entity_revisions
662
+ SET canonical_fingerprint = ?,
663
+ metadata_owned = CASE WHEN metadata_owned = 1 OR ? = 1 THEN 1 ELSE 0 END,
664
+ reopen_assignee_owned = CASE WHEN reopen_assignee_owned = 1 OR ? = 1 THEN 1 ELSE 0 END,
665
+ updated_at = ?
666
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
667
+ AND entity_revision = ? AND lifecycle_revision = ? AND last_sequence = ?
668
+ `).run(canonicalFingerprint, input.metadataOwned ? 1 : 0, input.reopenAssigneeOwned ? 1 : 0, now, tenantId, workspaceId, domain, entityId, entityRevision, lifecycleRevision, sequence);
669
+ if (Number(updated.changes || 0) !== 1) {
670
+ throw new WorkspaceSyncRevisionConflictError({
671
+ domain,
672
+ entityId,
673
+ reason: 'authoritative-equal-revision-replacement-raced',
674
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
675
+ });
676
+ }
677
+ return { state: 'applied', result };
678
+ }
679
+ return { state: 'current' };
680
+ }
681
+ if (current.lastSequence !== null && BigInt(sequence) <= BigInt(current.lastSequence)) {
682
+ throw new WorkspaceSyncRevisionConflictError({
683
+ domain,
684
+ entityId,
685
+ reason: 'sequence-regression',
686
+ incoming: { sequence, entityRevision, lifecycleRevision, canonicalFingerprint },
687
+ current,
688
+ });
689
+ }
690
+ return applyAuthoritativeRevision();
691
+ })();
692
+ }
693
+ runLocalTaskDeleteCaptureTransaction(input) {
694
+ const tenantId = requireText(input.tenantId, 'Tenant id');
695
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
696
+ const taskId = requireText(input.taskId, 'Task id');
697
+ const now = requireIsoTimestamp(input.now, 'Delete capture time');
698
+ return this.db.transaction(() => {
699
+ const lock = this.db.prepare(`
700
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
701
+ VALUES (?, ?, ?, ?)
702
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
703
+ `);
704
+ lock.run(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
705
+ lock.run(tenantId, workspaceId, 'task-relationship', now);
706
+ lock.run(tenantId, workspaceId, `local-outbox:task:${taskId}`, now);
707
+ return input.apply();
708
+ })();
709
+ }
710
+ runLocalTaskRestoreCaptureTransaction(input) {
711
+ const tenantId = requireText(input.tenantId, 'Tenant id');
712
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
713
+ const taskId = requireText(input.taskId, 'Task id');
714
+ const now = requireIsoTimestamp(input.now, 'Restore capture time');
715
+ return this.db.transaction(() => {
716
+ const lock = this.db.prepare(`
717
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
718
+ VALUES (?, ?, ?, ?)
719
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
720
+ `);
721
+ lock.run(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
722
+ lock.run(tenantId, workspaceId, `local-outbox:task:${taskId}`, now);
723
+ return input.apply();
724
+ })();
725
+ }
726
+ readOperation(input) {
727
+ const ids = operationParams(input.identity);
728
+ const requestFingerprint = requireText(input.requestFingerprint, 'Request fingerprint');
729
+ const row = this.readOperationRow(ids);
730
+ if (!row)
731
+ return null;
732
+ this.assertMatchingOperation(row, requestFingerprint);
733
+ if (row.status === 'committed') {
734
+ if (typeof row.response_json !== 'string')
735
+ throw new Error('Committed operation is missing its response.');
736
+ return {
737
+ state: 'committed',
738
+ responseJson: row.response_json,
739
+ acceptedSequence: row.accepted_sequence == null ? null : normalizeSequence(row.accepted_sequence),
740
+ };
741
+ }
742
+ return {
743
+ state: 'in_progress',
744
+ retryAt: requireIsoTimestamp(row.lease_expires_at, 'Existing lease expiry'),
745
+ };
746
+ }
747
+ readCommittedOperationResponse(identity) {
748
+ const row = this.db.prepare(`
749
+ SELECT response_json
750
+ FROM workspace_sync_operation_results
751
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
752
+ AND status = 'committed'
753
+ `).get(...operationParams(identity));
754
+ if (!row)
755
+ return null;
756
+ if (typeof row.response_json !== 'string')
757
+ throw new Error('Committed operation is missing its response.');
758
+ return row.response_json;
759
+ }
760
+ readOperationReservationMetadata(identity) {
761
+ const row = this.db.prepare(`
762
+ SELECT status, created_at
763
+ FROM workspace_sync_operation_results
764
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
765
+ `).get(...operationParams(identity));
766
+ if (!row)
767
+ return null;
768
+ if (row.status !== 'processing' && row.status !== 'committed') {
769
+ throw new Error('Workspace sync operation status is invalid.');
770
+ }
771
+ return {
772
+ status: row.status,
773
+ createdAt: requireIsoTimestamp(row.created_at, 'Operation reservation creation time'),
774
+ };
775
+ }
776
+ readOutboxOperation(identity) {
777
+ const row = this.db.prepare(`
778
+ SELECT tenant_id, workspace_id, client_id, operation_id, dispatch_class, domain, entity_id,
779
+ mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
780
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
781
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
782
+ payload_json, payload_fingerprint, status, attempt_count,
783
+ initial_not_found_attempt_count,
784
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
785
+ rebase_before_first_attempt, lease_owner, lease_expires_at
786
+ FROM workspace_sync_outbox
787
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
788
+ `).get(...operationParams(identity));
789
+ return row ? this.mapOutboxRow(row) : null;
790
+ }
791
+ readV3DispatchSettlementEvidence(identity) {
792
+ const row = this.db.prepare(`
793
+ SELECT outbox.status, outbox.authoritative_response_json,
794
+ CAST(outbox.accepted_sequence AS TEXT) AS accepted_sequence,
795
+ outbox.recovery_batch_id, recovery.state AS recovery_state,
796
+ recovery.baseline_marker, recovery.completed_at
797
+ FROM workspace_sync_outbox AS outbox
798
+ LEFT JOIN workspace_sync_outbox_recoveries AS recovery
799
+ ON recovery.tenant_id = outbox.tenant_id
800
+ AND recovery.workspace_id = outbox.workspace_id
801
+ AND recovery.recovery_batch_id = outbox.recovery_batch_id
802
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
803
+ AND outbox.client_id = ? AND outbox.operation_id = ?
804
+ `).get(...operationParams(identity));
805
+ if (!row)
806
+ return null;
807
+ if (row.status === 'accepted' && typeof row.authoritative_response_json === 'string') {
808
+ return {
809
+ kind: 'accepted',
810
+ acceptedSequence: row.accepted_sequence == null
811
+ ? null
812
+ : String(row.accepted_sequence),
813
+ };
814
+ }
815
+ if (row.status === 'superseded'
816
+ && row.recovery_state === 'completed'
817
+ && typeof row.recovery_batch_id === 'string'
818
+ && typeof row.baseline_marker === 'string'
819
+ && typeof row.completed_at === 'string') {
820
+ return {
821
+ kind: 'completed-recovery',
822
+ recoveryBatchId: row.recovery_batch_id,
823
+ baselineMarker: row.baseline_marker,
824
+ completedAt: requireIsoTimestamp(row.completed_at, 'Outbox recovery completion time'),
825
+ };
826
+ }
827
+ return null;
828
+ }
829
+ listBlockedInvalidOutboxOperations(input) {
830
+ const tenantId = requireText(input.tenantId, 'Tenant id');
831
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
832
+ const limit = Math.max(1, Math.min(100, Math.floor(input.limit ?? 100)));
833
+ const rows = this.db.prepare(`
834
+ SELECT tenant_id, workspace_id, client_id, operation_id, dispatch_class, domain, entity_id,
835
+ mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
836
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
837
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
838
+ payload_json, payload_fingerprint, status, attempt_count,
839
+ initial_not_found_attempt_count,
840
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
841
+ rebase_before_first_attempt, lease_owner, lease_expires_at
842
+ FROM workspace_sync_outbox
843
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
844
+ AND contract_version = ?
845
+ AND last_error_code = 'WORKSPACE_SYNC_OUTBOX_INVALID'
846
+ ORDER BY enqueue_sequence
847
+ LIMIT ?
848
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, limit);
849
+ return rows.map((row) => this.mapOutboxRow(row));
850
+ }
851
+ requeueBlockedInvalidOutboxOperation(input) {
852
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
853
+ const result = this.db.prepare(`
854
+ UPDATE workspace_sync_outbox
855
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
856
+ last_error_code = 'WORKSPACE_SYNC_OUTBOX_CONTRACT_UPGRADE_RECOVERY',
857
+ last_error_message = 'Requeued after the current local contract accepted the persisted operation.',
858
+ updated_at = ?
859
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
860
+ AND contract_version = ?
861
+ AND status = 'blocked' AND last_error_code = 'WORKSPACE_SYNC_OUTBOX_INVALID'
862
+ `).run(now, now, ...operationParams(input.identity), WORKSPACE_SYNC_CONTRACT_VERSION);
863
+ return Number(result.changes || 0) === 1;
864
+ }
865
+ requeueAcceptedV3DispatchOperation(input) {
866
+ const now = requireIsoTimestamp(input.now, 'Accepted replay recovery time');
867
+ const result = this.db.prepare(`
868
+ UPDATE workspace_sync_outbox
869
+ SET status = 'pending', next_attempt_at = ?,
870
+ lease_owner = NULL, lease_expires_at = NULL,
871
+ accepted_replay_recovery_count = accepted_replay_recovery_count + 1,
872
+ last_error_code = 'WORKSPACE_SYNC_ACCEPTED_REPLAY_RECOVERY',
873
+ last_error_message = 'Requeued the exact operation identity after the coordinator proved upstream acceptance before local acknowledgement committed.',
874
+ updated_at = ?
875
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
876
+ AND contract_version = ?
877
+ AND status = 'blocked'
878
+ AND accepted_replay_recovery_count < 1
879
+ AND recovery_batch_id IS NULL
880
+ `).run(now, now, ...operationParams(input.identity), WORKSPACE_SYNC_CONTRACT_VERSION);
881
+ return Number(result.changes || 0) === 1;
882
+ }
883
+ requeueWorkstreamInitiativeDependencyAcknowledgements(input) {
884
+ const tenantId = requireText(input.tenantId, 'Tenant id');
885
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
886
+ const now = requireIsoTimestamp(input.now, 'Workstream dependency recovery time');
887
+ const result = this.db.prepare(`
888
+ UPDATE workspace_sync_outbox
889
+ SET status = 'pending', next_attempt_at = ?,
890
+ lease_owner = NULL, lease_expires_at = NULL,
891
+ accepted_replay_recovery_count = accepted_replay_recovery_count + 1,
892
+ last_error_code = 'WORKSPACE_SYNC_WORKSTREAM_INITIATIVE_DEPENDENCY_RECOVERY',
893
+ last_error_message = 'Requeued the accepted workstream once after authoritative parent dependency support became available.',
894
+ updated_at = ?
895
+ WHERE tenant_id = ? AND workspace_id = ?
896
+ AND contract_version = ?
897
+ AND status = 'blocked'
898
+ AND domain = 'workstream' AND mutation_kind = 'metadata-update'
899
+ AND last_error_code = 'INITIATIVE_NOT_FOUND'
900
+ AND workstream_metadata_revision_rebase_count = 1
901
+ AND accepted_replay_recovery_count = 1
902
+ AND recovery_batch_id IS NULL
903
+ `).run(now, now, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
904
+ return Number(result.changes || 0);
905
+ }
906
+ readWorkflowAssignmentDependencyState(input) {
907
+ const tenantId = requireText(input.tenantId, 'Tenant id');
908
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
909
+ const executionId = requireText(input.executionId, 'Workflow execution id');
910
+ const beforeEnqueueSequence = normalizeSequence(input.beforeEnqueueSequence);
911
+ const rows = this.db.prepare(`
912
+ SELECT status, payload_json
913
+ FROM workspace_sync_outbox
914
+ WHERE tenant_id = ? AND workspace_id = ?
915
+ AND domain = 'task' AND mutation_kind = 'set-status'
916
+ AND status IN ('pending', 'in_flight', 'blocked')
917
+ AND enqueue_sequence < ?
918
+ ORDER BY CASE status WHEN 'blocked' THEN 0 ELSE 1 END, enqueue_sequence
919
+ `).all(tenantId, workspaceId, beforeEnqueueSequence);
920
+ for (const row of rows) {
921
+ try {
922
+ const payload = JSON.parse(String(row.payload_json || ''));
923
+ if (String(payload.workflowAssignment?.execution?.id || '') !== executionId)
924
+ continue;
925
+ return row.status === 'blocked' ? 'blocked' : 'pending';
926
+ }
927
+ catch {
928
+ // Invalid payloads are handled by their owning outbox handler.
929
+ }
930
+ }
931
+ return null;
932
+ }
933
+ getActiveEntityBusyError(input) {
934
+ const tenantId = requireText(input.tenantId, 'Tenant id');
935
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
936
+ const target = {
937
+ domain: requireText(input.target.domain, 'Target domain'),
938
+ entityId: requireText(input.target.entityId, 'Target entity id'),
939
+ };
940
+ const mutationKind = String(input.mutationKind || '').trim();
941
+ const now = requireIsoTimestamp(input.now, 'Current time');
942
+ const active = this.db.prepare(`
943
+ SELECT outbox.operation_id, outbox.domain, outbox.entity_id, outbox.mutation_kind,
944
+ outbox.status, outbox.attempt_count, outbox.next_attempt_at,
945
+ outbox.lease_expires_at, outbox.last_error_code,
946
+ outbox.created_at, outbox.updated_at
947
+ FROM workspace_sync_outbox AS outbox
948
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
949
+ AND outbox.status IN ('pending', 'in_flight', 'blocked')
950
+ AND (? = '' OR outbox.mutation_kind = ?)
951
+ AND (
952
+ (outbox.domain = ? AND outbox.entity_id = ?)
953
+ OR EXISTS (
954
+ SELECT 1 FROM workspace_sync_outbox_targets AS persisted_target
955
+ WHERE persisted_target.tenant_id = outbox.tenant_id
956
+ AND persisted_target.workspace_id = outbox.workspace_id
957
+ AND persisted_target.client_id = outbox.client_id
958
+ AND persisted_target.operation_id = outbox.operation_id
959
+ AND persisted_target.domain = ? AND persisted_target.entity_id = ?
960
+ )
961
+ )
962
+ ORDER BY CASE outbox.status
963
+ WHEN 'blocked' THEN 0
964
+ WHEN 'in_flight' THEN 1
965
+ ELSE 2
966
+ END,
967
+ outbox.enqueue_sequence
968
+ `).all(tenantId, workspaceId, mutationKind, mutationKind, target.domain, target.entityId, target.domain, target.entityId);
969
+ return active.length > 0
970
+ ? workspaceSyncEntityBusyError({ target, active, now })
971
+ : null;
972
+ }
973
+ hasActiveOutboxSuccessor(identity) {
974
+ const ids = operationParams(identity);
975
+ const current = this.db.prepare(`
976
+ SELECT domain, entity_id, enqueue_sequence
977
+ FROM workspace_sync_outbox
978
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
979
+ `).get(...ids);
980
+ if (!current)
981
+ throw new Error('Workspace sync outbox operation does not exist.');
982
+ const successor = this.db.prepare(`
983
+ SELECT 1 AS active
984
+ FROM workspace_sync_outbox AS successor
985
+ WHERE successor.tenant_id = ? AND successor.workspace_id = ?
986
+ AND successor.status IN ('pending', 'in_flight', 'blocked')
987
+ AND successor.enqueue_sequence > ?
988
+ AND EXISTS (
989
+ SELECT 1
990
+ FROM workspace_sync_outbox_targets AS current_target
991
+ INNER JOIN workspace_sync_outbox_targets AS successor_target
992
+ ON successor_target.tenant_id = current_target.tenant_id
993
+ AND successor_target.workspace_id = current_target.workspace_id
994
+ AND successor_target.domain = current_target.domain
995
+ AND successor_target.entity_id = current_target.entity_id
996
+ WHERE current_target.tenant_id = ? AND current_target.workspace_id = ?
997
+ AND current_target.client_id = ? AND current_target.operation_id = ?
998
+ AND successor_target.client_id = successor.client_id
999
+ AND successor_target.operation_id = successor.operation_id
1000
+ )
1001
+ LIMIT 1
1002
+ `).get(ids[0], ids[1], normalizeSequence(current.enqueue_sequence ?? 0), ...ids);
1003
+ return Number(successor?.active || 0) === 1;
1004
+ }
1005
+ listActiveOutboxSuccessors(identity) {
1006
+ const ids = operationParams(identity);
1007
+ const current = this.db.prepare(`
1008
+ SELECT domain, entity_id, enqueue_sequence
1009
+ FROM workspace_sync_outbox
1010
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1011
+ `).get(...ids);
1012
+ if (!current)
1013
+ throw new Error('Workspace sync outbox operation does not exist.');
1014
+ const rows = this.db.prepare(`
1015
+ SELECT tenant_id, workspace_id, client_id, operation_id, dispatch_class, domain, entity_id,
1016
+ mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1017
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
1018
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
1019
+ payload_json, payload_fingerprint, status, attempt_count,
1020
+ initial_not_found_attempt_count,
1021
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
1022
+ rebase_before_first_attempt, lease_owner, lease_expires_at
1023
+ FROM workspace_sync_outbox
1024
+ WHERE tenant_id = ? AND workspace_id = ?
1025
+ AND status IN ('pending', 'in_flight', 'blocked')
1026
+ AND enqueue_sequence > ?
1027
+ AND EXISTS (
1028
+ SELECT 1
1029
+ FROM workspace_sync_outbox_targets AS current_target
1030
+ INNER JOIN workspace_sync_outbox_targets AS successor_target
1031
+ ON successor_target.tenant_id = current_target.tenant_id
1032
+ AND successor_target.workspace_id = current_target.workspace_id
1033
+ AND successor_target.domain = current_target.domain
1034
+ AND successor_target.entity_id = current_target.entity_id
1035
+ WHERE current_target.tenant_id = ? AND current_target.workspace_id = ?
1036
+ AND current_target.client_id = ? AND current_target.operation_id = ?
1037
+ AND successor_target.client_id = workspace_sync_outbox.client_id
1038
+ AND successor_target.operation_id = workspace_sync_outbox.operation_id
1039
+ )
1040
+ ORDER BY enqueue_sequence
1041
+ `).all(ids[0], ids[1], normalizeSequence(current.enqueue_sequence ?? 0), ...ids);
1042
+ return rows.map((row) => this.mapOutboxRow(row));
1043
+ }
1044
+ supersedeBlockedOutboxChain(input) {
1045
+ const ids = operationParams(input.identity);
1046
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1047
+ const now = requireIsoTimestamp(input.now, 'Supersession time');
1048
+ const matchesRecoveryBatch = (recovery) => Boolean(recovery
1049
+ && recovery.state === 'awaiting_baseline'
1050
+ && (input.appendToExistingRecovery
1051
+ || (recovery.root_client_id === ids[2] && recovery.root_operation_id === ids[3])));
1052
+ const listRecoveryOperationIds = () => {
1053
+ const rows = this.db.prepare(`
1054
+ SELECT operation_id
1055
+ FROM workspace_sync_outbox
1056
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1057
+ AND status = 'superseded'
1058
+ ORDER BY enqueue_sequence, created_at, operation_id
1059
+ `).all(ids[0], ids[1], recoveryBatchId);
1060
+ return rows.map((row) => requireText(row.operation_id, 'Operation id'));
1061
+ };
1062
+ return this.db.transaction(() => {
1063
+ const root = this.db.prepare(`
1064
+ SELECT status, recovery_batch_id,
1065
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence
1066
+ FROM workspace_sync_outbox
1067
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1068
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1069
+ `).get(...ids);
1070
+ if (!root)
1071
+ throw new Error('Workspace sync outbox operation does not exist.');
1072
+ if (root.status === 'superseded') {
1073
+ if (root.recovery_batch_id !== recoveryBatchId) {
1074
+ throw new Error('Workspace sync outbox operation belongs to a different recovery batch.');
1075
+ }
1076
+ const recovery = this.db.prepare(`
1077
+ SELECT root_client_id, root_operation_id, state
1078
+ FROM workspace_sync_outbox_recoveries
1079
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1080
+ `).get(ids[0], ids[1], recoveryBatchId);
1081
+ if (!matchesRecoveryBatch(recovery)) {
1082
+ throw new Error('Workspace sync outbox recovery batch root does not match.');
1083
+ }
1084
+ return {
1085
+ recoveryBatchId,
1086
+ operationIds: listRecoveryOperationIds(),
1087
+ };
1088
+ }
1089
+ if (root.status !== 'blocked') {
1090
+ throw new Error('Only a blocked workspace sync operation can start recovery.');
1091
+ }
1092
+ const rootKey = `${ids[2]}\0${ids[3]}`;
1093
+ const affectedOperationKeys = new Set([rootKey]);
1094
+ const affectedTargets = new Set();
1095
+ const lockedTargets = new Set();
1096
+ const lockTarget = this.db.prepare(`
1097
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
1098
+ VALUES (?, ?, ?, ?)
1099
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
1100
+ `);
1101
+ let candidates = [];
1102
+ let targetsByOperation = new Map();
1103
+ while (true) {
1104
+ const targets = this.db.prepare(`
1105
+ SELECT client_id, operation_id, domain, entity_id
1106
+ FROM workspace_sync_outbox_targets
1107
+ WHERE tenant_id = ? AND workspace_id = ?
1108
+ `).all(ids[0], ids[1]);
1109
+ targetsByOperation = new Map();
1110
+ for (const target of targets) {
1111
+ const operationKey = `${requireText(target.client_id, 'Client id')}\0${requireText(target.operation_id, 'Operation id')}`;
1112
+ const operationTargets = targetsByOperation.get(operationKey) || new Set();
1113
+ operationTargets.add(`${requireText(target.domain, 'Domain')}\0${requireText(target.entity_id, 'Entity id')}`);
1114
+ targetsByOperation.set(operationKey, operationTargets);
1115
+ }
1116
+ for (const target of targetsByOperation.get(rootKey) || [])
1117
+ affectedTargets.add(target);
1118
+ if (affectedTargets.size === 0) {
1119
+ throw new Error('Blocked workspace sync recovery root has no durable targets.');
1120
+ }
1121
+ const unlockedTargets = [...affectedTargets]
1122
+ .filter((target) => !lockedTargets.has(target))
1123
+ .sort(compareCodeUnits);
1124
+ for (const target of unlockedTargets) {
1125
+ const separator = target.indexOf('\0');
1126
+ const domain = target.slice(0, separator);
1127
+ const entityId = target.slice(separator + 1);
1128
+ lockTarget.run(ids[0], ids[1], `local-outbox:${domain}:${entityId}`, now);
1129
+ lockedTargets.add(target);
1130
+ }
1131
+ candidates = this.db.prepare(`
1132
+ SELECT client_id, operation_id, dispatch_class, domain, entity_id, mutation_kind,
1133
+ payload_json, payload_fingerprint, status, lease_expires_at,
1134
+ last_error_code, last_error_message,
1135
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
1136
+ created_at
1137
+ FROM workspace_sync_outbox
1138
+ WHERE tenant_id = ? AND workspace_id = ?
1139
+ AND status IN ('pending', 'in_flight', 'blocked')
1140
+ AND enqueue_sequence >= ?
1141
+ ORDER BY enqueue_sequence, created_at, operation_id
1142
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1143
+ `).all(ids[0], ids[1], normalizeSequence(root.enqueue_sequence ?? 0));
1144
+ let changed = true;
1145
+ while (changed) {
1146
+ changed = false;
1147
+ for (const candidate of candidates) {
1148
+ const candidateKey = `${requireText(candidate.client_id, 'Client id')}\0${requireText(candidate.operation_id, 'Operation id')}`;
1149
+ if (affectedOperationKeys.has(candidateKey))
1150
+ continue;
1151
+ const candidateTargets = targetsByOperation.get(candidateKey) || new Set();
1152
+ if (![...candidateTargets].some((target) => affectedTargets.has(target)))
1153
+ continue;
1154
+ affectedOperationKeys.add(candidateKey);
1155
+ for (const target of candidateTargets)
1156
+ affectedTargets.add(target);
1157
+ changed = true;
1158
+ }
1159
+ }
1160
+ if ([...affectedTargets].every((target) => lockedTargets.has(target)))
1161
+ break;
1162
+ }
1163
+ const selected = candidates.filter((candidate) => affectedOperationKeys.has(`${requireText(candidate.client_id, 'Client id')}\0${requireText(candidate.operation_id, 'Operation id')}`));
1164
+ if (!selected.some((candidate) => (candidate.client_id === ids[2] && candidate.operation_id === ids[3]))) {
1165
+ throw new Error('Blocked workspace sync recovery root was not selected.');
1166
+ }
1167
+ const activeLease = selected.find((candidate) => (candidate.status === 'in_flight'
1168
+ && Date.parse(String(candidate.lease_expires_at || '')) > Date.parse(now)));
1169
+ if (activeLease) {
1170
+ throw new Error('A workspace sync operation in the recovery chain still owns an active lease.');
1171
+ }
1172
+ if (input.replayPolicy === 'task-root') {
1173
+ for (const candidate of selected) {
1174
+ const supportedTaskRoot = candidate.domain === 'task'
1175
+ && TASK_ROOT_SUCCESSOR_MUTATIONS.has(String(candidate.mutation_kind || ''));
1176
+ const supportedTaskComment = candidate.domain === 'task-comments'
1177
+ && candidate.mutation_kind === 'append';
1178
+ const supportedTaskCreate = candidate.domain === 'task'
1179
+ && candidate.mutation_kind === 'create'
1180
+ && TASK_CREATE_RECOVERY_ERROR_CODES.has(String(candidate.last_error_code || ''));
1181
+ const supportedTaskAttachmentLinks = candidate.domain === 'task-attachment-links'
1182
+ && candidate.mutation_kind === 'replace';
1183
+ const supportedEntityEventAdoption = candidate.dispatch_class === 'legacy-adoption'
1184
+ && candidate.domain === 'entity-event'
1185
+ && candidate.mutation_kind === 'adopt-legacy-batch';
1186
+ if (!supportedTaskRoot && !supportedTaskComment
1187
+ && !supportedTaskCreate && !supportedTaskAttachmentLinks
1188
+ && !supportedEntityEventAdoption) {
1189
+ throw new Error('Workspace sync recovery replay currently supports task-root, task-comment append, '
1190
+ + 'task-create attachment-link, and legacy entity-event adoption intents only.');
1191
+ }
1192
+ if (supportedTaskCreate)
1193
+ this.readTaskCreateRecoveryIntent(candidate);
1194
+ else if (supportedTaskAttachmentLinks) {
1195
+ this.readTaskAttachmentLinksRecoveryIntent(candidate);
1196
+ }
1197
+ else if (supportedTaskComment
1198
+ && (candidate.client_id !== ids[2] || candidate.operation_id !== ids[3])) {
1199
+ // A successor comment still owns the active operation that will be
1200
+ // superseded with this chain. Validate its frozen intent now, then
1201
+ // require authoritative event ownership when the replay is prepared.
1202
+ this.readTaskCommentAppendRecoveryIntent(ids[1], candidate);
1203
+ }
1204
+ else {
1205
+ this.prepareRecoveryPayload(ids[0], ids[1], candidate);
1206
+ }
1207
+ }
1208
+ }
1209
+ this.db.prepare(`
1210
+ INSERT INTO workspace_sync_outbox_recoveries (
1211
+ tenant_id, workspace_id, recovery_batch_id,
1212
+ root_client_id, root_operation_id, state,
1213
+ created_at, updated_at
1214
+ ) VALUES (?, ?, ?, ?, ?, 'awaiting_baseline', ?, ?)
1215
+ ON CONFLICT (tenant_id, workspace_id, recovery_batch_id) DO NOTHING
1216
+ `).run(ids[0], ids[1], recoveryBatchId, ids[2], ids[3], now, now);
1217
+ const recovery = this.db.prepare(`
1218
+ SELECT root_client_id, root_operation_id, state
1219
+ FROM workspace_sync_outbox_recoveries
1220
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1221
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1222
+ `).get(ids[0], ids[1], recoveryBatchId);
1223
+ if (!matchesRecoveryBatch(recovery)) {
1224
+ throw new Error('Workspace sync recovery batch identity is already in use.');
1225
+ }
1226
+ for (const candidate of selected) {
1227
+ const clientId = requireText(candidate.client_id, 'Client id');
1228
+ const operationId = requireText(candidate.operation_id, 'Operation id');
1229
+ const result = this.db.prepare(`
1230
+ UPDATE workspace_sync_outbox
1231
+ SET status = 'superseded', recovery_batch_id = ?, superseded_at = ?,
1232
+ lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
1233
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1234
+ AND (
1235
+ status IN ('pending', 'blocked')
1236
+ OR (status = 'in_flight' AND lease_expires_at <= ?)
1237
+ )
1238
+ `).run(recoveryBatchId, now, now, ids[0], ids[1], clientId, operationId, now);
1239
+ if (Number(result.changes || 0) !== 1) {
1240
+ throw new Error('Workspace sync recovery chain changed while it was being superseded.');
1241
+ }
1242
+ }
1243
+ return {
1244
+ recoveryBatchId,
1245
+ operationIds: listRecoveryOperationIds(),
1246
+ };
1247
+ })();
1248
+ }
1249
+ recordOutboxRecoveryBaseline(input) {
1250
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1251
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1252
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1253
+ const baselineMarker = requireText(input.baselineMarker, 'Recovery baseline marker');
1254
+ const now = requireIsoTimestamp(input.now, 'Recovery baseline time');
1255
+ this.db.transaction(() => {
1256
+ const recovery = this.db.prepare(`
1257
+ SELECT state, baseline_marker
1258
+ FROM workspace_sync_outbox_recoveries
1259
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1260
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1261
+ `).get(tenantId, workspaceId, recoveryBatchId);
1262
+ if (!recovery)
1263
+ throw new Error('Workspace sync recovery batch does not exist.');
1264
+ if (recovery.state === 'baseline_ready' || recovery.state === 'replay_queued') {
1265
+ if (recovery.baseline_marker !== baselineMarker) {
1266
+ throw new Error('Workspace sync recovery baseline marker does not match.');
1267
+ }
1268
+ return;
1269
+ }
1270
+ if (recovery.state !== 'awaiting_baseline') {
1271
+ throw new Error('Workspace sync recovery is not waiting for an authoritative baseline.');
1272
+ }
1273
+ const result = this.db.prepare(`
1274
+ UPDATE workspace_sync_outbox_recoveries
1275
+ SET state = 'baseline_ready', baseline_marker = ?,
1276
+ baseline_applied_at = ?, updated_at = ?
1277
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1278
+ AND state = 'awaiting_baseline'
1279
+ `).run(baselineMarker, now, now, tenantId, workspaceId, recoveryBatchId);
1280
+ if (Number(result.changes || 0) !== 1) {
1281
+ throw new Error('Workspace sync recovery phase changed while recording its baseline.');
1282
+ }
1283
+ })();
1284
+ }
1285
+ listOutboxRecoveryTargets(input) {
1286
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1287
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1288
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1289
+ const rows = this.db.prepare(`
1290
+ SELECT DISTINCT target.domain, target.entity_id
1291
+ FROM workspace_sync_outbox AS source
1292
+ INNER JOIN workspace_sync_outbox_targets AS target
1293
+ ON target.tenant_id = source.tenant_id
1294
+ AND target.workspace_id = source.workspace_id
1295
+ AND target.client_id = source.client_id
1296
+ AND target.operation_id = source.operation_id
1297
+ WHERE source.tenant_id = ? AND source.workspace_id = ?
1298
+ AND source.recovery_batch_id = ?
1299
+ AND source.status = 'superseded'
1300
+ AND source.replay_of_operation_id IS NULL
1301
+ ORDER BY target.domain, target.entity_id
1302
+ `).all(tenantId, workspaceId, recoveryBatchId);
1303
+ if (rows.length === 0)
1304
+ throw new Error('Workspace sync recovery snapshot has no targets.');
1305
+ return rows.map((row) => ({
1306
+ domain: requireText(row.domain, 'Recovery target domain'),
1307
+ entityId: requireText(row.entity_id, 'Recovery target entity id'),
1308
+ }));
1309
+ }
1310
+ prepareTaskRootOutboxRecoveryReplay(input) {
1311
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1312
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1313
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1314
+ const now = requireIsoTimestamp(input.now, 'Recovery replay time');
1315
+ return this.db.transaction(() => {
1316
+ const recovery = this.db.prepare(`
1317
+ SELECT state, baseline_marker
1318
+ FROM workspace_sync_outbox_recoveries
1319
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1320
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1321
+ `).get(tenantId, workspaceId, recoveryBatchId);
1322
+ if (!recovery)
1323
+ throw new Error('Workspace sync recovery batch does not exist.');
1324
+ const listReplayOperationIds = () => {
1325
+ const rows = this.db.prepare(`
1326
+ SELECT operation_id
1327
+ FROM workspace_sync_outbox
1328
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1329
+ AND replay_of_operation_id IS NOT NULL
1330
+ ORDER BY enqueue_sequence, created_at, operation_id
1331
+ `).all(tenantId, workspaceId, recoveryBatchId);
1332
+ return rows.map((row) => requireText(row.operation_id, 'Replay operation id'));
1333
+ };
1334
+ if (recovery.state === 'replay_queued') {
1335
+ return { recoveryBatchId, operationIds: listReplayOperationIds() };
1336
+ }
1337
+ if (recovery.state !== 'baseline_ready') {
1338
+ throw new Error('Workspace sync recovery requires an authoritative baseline before replay.');
1339
+ }
1340
+ const baselineMarker = requireText(recovery.baseline_marker, 'Workspace sync recovery baseline marker');
1341
+ const recoverySources = this.db.prepare(`
1342
+ SELECT client_id, operation_id, dispatch_class, domain, entity_id, mutation_kind,
1343
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1344
+ payload_json, payload_fingerprint, last_error_code, last_error_message,
1345
+ EXISTS (
1346
+ SELECT 1
1347
+ FROM workspace_sync_outbox AS replay_candidate
1348
+ WHERE replay_candidate.tenant_id = workspace_sync_outbox.tenant_id
1349
+ AND replay_candidate.workspace_id = workspace_sync_outbox.workspace_id
1350
+ AND replay_candidate.recovery_batch_id = workspace_sync_outbox.recovery_batch_id
1351
+ AND replay_candidate.replay_of_client_id = workspace_sync_outbox.client_id
1352
+ AND replay_candidate.replay_of_operation_id = workspace_sync_outbox.operation_id
1353
+ AND replay_candidate.status = 'superseded'
1354
+ AND (
1355
+ (
1356
+ workspace_sync_outbox.mutation_kind = 'archive'
1357
+ AND replay_candidate.last_error_code = 'TASK_LIFECYCLE_ALREADY_ARCHIVED'
1358
+ )
1359
+ OR (
1360
+ workspace_sync_outbox.mutation_kind = 'unarchive'
1361
+ AND replay_candidate.last_error_code = 'TASK_LIFECYCLE_NOT_ARCHIVED'
1362
+ )
1363
+ OR (
1364
+ workspace_sync_outbox.mutation_kind = 'archive'
1365
+ AND replay_candidate.last_error_code = 'TASK_LIFECYCLE_STATE_CONFLICT'
1366
+ AND replay_candidate.last_error_message = 'Task is already archived.'
1367
+ )
1368
+ OR (
1369
+ workspace_sync_outbox.mutation_kind = 'unarchive'
1370
+ AND replay_candidate.last_error_code = 'TASK_LIFECYCLE_STATE_CONFLICT'
1371
+ AND replay_candidate.last_error_message = 'Task is not archived.'
1372
+ )
1373
+ )
1374
+ ) AS baseline_satisfaction_candidate,
1375
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence
1376
+ FROM workspace_sync_outbox
1377
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1378
+ AND status = 'superseded' AND replay_of_operation_id IS NULL
1379
+ ORDER BY enqueue_sequence, created_at, operation_id
1380
+ `).all(tenantId, workspaceId, recoveryBatchId);
1381
+ for (const source of recoverySources) {
1382
+ if (source.domain === 'task-comments' && source.mutation_kind === 'append') {
1383
+ this.assertTaskCommentAppendRecoveryEventOwned(tenantId, workspaceId, source);
1384
+ }
1385
+ else if (source.domain === 'task' && source.mutation_kind === 'create') {
1386
+ this.readTaskCreateRecoveryIntent(source);
1387
+ }
1388
+ else if (source.domain === 'task-attachment-links'
1389
+ && source.mutation_kind === 'replace') {
1390
+ this.readTaskAttachmentLinksRecoveryIntent(source);
1391
+ }
1392
+ else if (source.dispatch_class === 'legacy-adoption'
1393
+ && source.domain === 'entity-event'
1394
+ && source.mutation_kind === 'adopt-legacy-batch') {
1395
+ this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source);
1396
+ }
1397
+ }
1398
+ const latestAttachmentSourceByEntity = new Map();
1399
+ for (const source of recoverySources) {
1400
+ if (source.domain === 'task-attachment-links' && source.mutation_kind === 'replace') {
1401
+ latestAttachmentSourceByEntity.set(requireText(source.entity_id, 'Source entity id'), source);
1402
+ }
1403
+ }
1404
+ const sources = recoverySources.filter((source) => {
1405
+ if (source.domain === 'task-comments' && source.mutation_kind === 'append') {
1406
+ return !this.isTaskCommentAppendRecoveryIntentSatisfied(tenantId, workspaceId, source);
1407
+ }
1408
+ if (source.domain === 'task' && source.mutation_kind === 'create') {
1409
+ this.assertTaskCreateRecoveryIntentSatisfied(tenantId, workspaceId, source);
1410
+ return false;
1411
+ }
1412
+ if (source.domain === 'task-attachment-links'
1413
+ && source.mutation_kind === 'replace') {
1414
+ if (latestAttachmentSourceByEntity.get(requireText(source.entity_id, 'Source entity id')) !== source) {
1415
+ return false;
1416
+ }
1417
+ return !this.isTaskAttachmentLinksRecoveryIntentSatisfied(tenantId, workspaceId, source);
1418
+ }
1419
+ if (source.dispatch_class === 'legacy-adoption'
1420
+ && source.domain === 'entity-event'
1421
+ && source.mutation_kind === 'adopt-legacy-batch') {
1422
+ // Legacy adoption migrates immutable historical activity; it is
1423
+ // not a user-authored mutation that can be rebased. A targeted
1424
+ // authoritative baseline proves that every event identity exists
1425
+ // in cloud and installs the canonical cloud body locally. Trying
1426
+ // to replay the older frozen body can only re-create the same
1427
+ // identity/revision conflict forever, so the baseline itself
1428
+ // satisfies this migration intent.
1429
+ return false;
1430
+ }
1431
+ if (Number(source.baseline_satisfaction_candidate || 0) !== 1) {
1432
+ return true;
1433
+ }
1434
+ return !this.isTaskLifecycleRecoveryIntentSatisfied(tenantId, workspaceId, source);
1435
+ });
1436
+ if (sources.length === 0) {
1437
+ const completed = this.db.prepare(`
1438
+ UPDATE workspace_sync_outbox_recoveries
1439
+ SET state = 'completed', completed_at = ?, updated_at = ?
1440
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1441
+ AND state = 'baseline_ready'
1442
+ `).run(now, now, tenantId, workspaceId, recoveryBatchId);
1443
+ if (Number(completed.changes || 0) !== 1) {
1444
+ throw new Error('Workspace sync recovery phase changed while recording baseline satisfaction.');
1445
+ }
1446
+ return { recoveryBatchId, operationIds: [] };
1447
+ }
1448
+ const newerActive = this.db.prepare(`
1449
+ SELECT active.operation_id
1450
+ FROM workspace_sync_outbox AS active
1451
+ WHERE active.tenant_id = ? AND active.workspace_id = ?
1452
+ AND active.status IN ('pending', 'in_flight', 'blocked')
1453
+ AND COALESCE(active.recovery_batch_id, '') <> ?
1454
+ AND EXISTS (
1455
+ SELECT 1
1456
+ FROM workspace_sync_outbox_targets AS recovered_target
1457
+ INNER JOIN workspace_sync_outbox AS recovered
1458
+ ON recovered.tenant_id = recovered_target.tenant_id
1459
+ AND recovered.workspace_id = recovered_target.workspace_id
1460
+ AND recovered.client_id = recovered_target.client_id
1461
+ AND recovered.operation_id = recovered_target.operation_id
1462
+ INNER JOIN workspace_sync_outbox_targets AS active_target
1463
+ ON active_target.tenant_id = recovered_target.tenant_id
1464
+ AND active_target.workspace_id = recovered_target.workspace_id
1465
+ AND active_target.domain = recovered_target.domain
1466
+ AND active_target.entity_id = recovered_target.entity_id
1467
+ WHERE recovered.tenant_id = active.tenant_id
1468
+ AND recovered.workspace_id = active.workspace_id
1469
+ AND recovered.recovery_batch_id = ?
1470
+ AND recovered.status = 'superseded'
1471
+ AND active_target.client_id = active.client_id
1472
+ AND active_target.operation_id = active.operation_id
1473
+ )
1474
+ LIMIT 1
1475
+ `).get(tenantId, workspaceId, recoveryBatchId, recoveryBatchId);
1476
+ if (newerActive) {
1477
+ throw new Error('Workspace sync recovery has newer active intent that requires semantic reconciliation.');
1478
+ }
1479
+ const replayPayloads = new Map();
1480
+ for (const source of sources) {
1481
+ const supportedTaskRoot = source.domain === 'task'
1482
+ && TASK_ROOT_SUCCESSOR_MUTATIONS.has(String(source.mutation_kind || ''));
1483
+ const supportedTaskComment = source.domain === 'task-comments'
1484
+ && source.mutation_kind === 'append';
1485
+ const supportedTaskAttachmentLinks = source.domain === 'task-attachment-links'
1486
+ && source.mutation_kind === 'replace';
1487
+ const supportedEntityEventAdoption = source.dispatch_class === 'legacy-adoption'
1488
+ && source.domain === 'entity-event'
1489
+ && source.mutation_kind === 'adopt-legacy-batch';
1490
+ if (!supportedTaskRoot && !supportedTaskComment && !supportedTaskAttachmentLinks
1491
+ && !supportedEntityEventAdoption) {
1492
+ throw new Error('Workspace sync recovery replay currently supports task-root, task-comment append, '
1493
+ + 'task attachment-link, and legacy entity-event adoption intents only.');
1494
+ }
1495
+ replayPayloads.set(`${requireText(source.client_id, 'Source client id')}\0${requireText(source.operation_id, 'Source operation id')}`, this.prepareRecoveryPayload(tenantId, workspaceId, source));
1496
+ }
1497
+ for (const source of sources) {
1498
+ const sourceClientId = requireText(source.client_id, 'Source client id');
1499
+ const sourceOperationId = requireText(source.operation_id, 'Source operation id');
1500
+ const operationId = source.dispatch_class === 'legacy-adoption'
1501
+ && source.domain === 'entity-event'
1502
+ && source.mutation_kind === 'adopt-legacy-batch'
1503
+ ? buildEntityEventAdoptionRecoveryReplayOperationId(recoveryBatchId, baselineMarker, sourceClientId, sourceOperationId)
1504
+ : buildRecoveryReplayOperationId(recoveryBatchId, baselineMarker, sourceClientId, sourceOperationId);
1505
+ const replayPayload = replayPayloads.get(`${sourceClientId}\0${sourceOperationId}`);
1506
+ if (!replayPayload)
1507
+ throw new Error('Workspace sync recovery replay payload is missing.');
1508
+ const sourceTargets = this.db.prepare(`
1509
+ SELECT domain, entity_id, mutation_kind,
1510
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee
1511
+ FROM workspace_sync_outbox_targets
1512
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1513
+ ORDER BY domain, entity_id
1514
+ `).all(tenantId, workspaceId, sourceClientId, sourceOperationId);
1515
+ const replayTargets = source.domain === 'task-comments'
1516
+ && source.mutation_kind === 'append'
1517
+ ? sourceTargets.filter((target) => (target.domain === 'task-comments'
1518
+ && target.entity_id === source.entity_id))
1519
+ : sourceTargets;
1520
+ if (replayTargets.length === 0)
1521
+ throw new Error('Workspace sync recovery source has no targets.');
1522
+ const targetRevisions = replayTargets.map((target) => ({
1523
+ target,
1524
+ revision: this.readEntityRevisionCoordinates({
1525
+ tenantId,
1526
+ workspaceId,
1527
+ domain: requireText(target.domain, 'Target domain'),
1528
+ entityId: requireText(target.entity_id, 'Target entity id'),
1529
+ }),
1530
+ }));
1531
+ const primary = targetRevisions.find(({ target }) => (target.domain === source.domain && target.entity_id === source.entity_id));
1532
+ if (!primary)
1533
+ throw new Error('Workspace sync recovery source primary target is missing.');
1534
+ const enqueueSequence = this.allocateOutboxEnqueueSequence(tenantId, workspaceId, now);
1535
+ this.db.prepare(`
1536
+ INSERT INTO workspace_sync_outbox (
1537
+ tenant_id, workspace_id, operation_id, contract_version, client_id,
1538
+ dispatch_class, domain, entity_id, mutation_kind,
1539
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1540
+ base_entity_revision, base_lifecycle_revision,
1541
+ payload_json, payload_fingerprint, status, attempt_count,
1542
+ initial_not_found_attempt_count, enqueue_sequence, rebase_before_first_attempt,
1543
+ next_attempt_at, recovery_batch_id,
1544
+ replay_of_client_id, replay_of_operation_id, created_at, updated_at
1545
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, 0, ?, 1,
1546
+ ?, ?, ?, ?, ?, ?)
1547
+ ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
1548
+ `).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), requireText(source.entity_id, 'Source entity id'), replayPayload.replayMutationKind, Number(source.owns_task_metadata) === 1 ? 1 : 0, Number(source.owns_task_lifecycle) === 1 ? 1 : 0, Number(source.owns_task_assignee) === 1 ? 1 : 0, primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, enqueueSequence, now, recoveryBatchId, sourceClientId, sourceOperationId, now, now);
1549
+ this.db.prepare(`
1550
+ UPDATE workspace_sync_outbox
1551
+ SET base_entity_revision = ?, base_lifecycle_revision = ?,
1552
+ payload_json = ?, payload_fingerprint = ?,
1553
+ status = 'pending', attempt_count = 0, initial_not_found_attempt_count = 0,
1554
+ rebase_before_first_attempt = 1, next_attempt_at = ?,
1555
+ lease_owner = NULL, lease_expires_at = NULL,
1556
+ authoritative_response_json = NULL, accepted_sequence = NULL,
1557
+ last_error_code = NULL, last_error_message = NULL,
1558
+ superseded_at = NULL, updated_at = ?
1559
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1560
+ AND recovery_batch_id = ? AND replay_of_client_id = ? AND replay_of_operation_id = ?
1561
+ AND status = 'superseded'
1562
+ `).run(primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, now, now, tenantId, workspaceId, sourceClientId, operationId, recoveryBatchId, sourceClientId, sourceOperationId);
1563
+ if (replayPayload.workflowDependencyCoalesced) {
1564
+ this.db.prepare(`
1565
+ UPDATE workspace_sync_outbox
1566
+ SET last_error_code = 'WORKSPACE_SYNC_WORKFLOW_DEPENDENCY_COALESCED',
1567
+ last_error_message = 'Replayed after an earlier accepted operation satisfied the exact workflow activation dependency.',
1568
+ updated_at = ?
1569
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1570
+ `).run(now, tenantId, workspaceId, sourceClientId, operationId);
1571
+ }
1572
+ const replayRow = this.db.prepare(`
1573
+ SELECT domain, entity_id, mutation_kind, payload_fingerprint,
1574
+ recovery_batch_id, replay_of_client_id, replay_of_operation_id
1575
+ FROM workspace_sync_outbox
1576
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1577
+ `).get(tenantId, workspaceId, sourceClientId, operationId);
1578
+ if (!replayRow
1579
+ || replayRow.domain !== source.domain
1580
+ || replayRow.entity_id !== source.entity_id
1581
+ || replayRow.mutation_kind !== replayPayload.replayMutationKind
1582
+ || replayRow.payload_fingerprint !== replayPayload.payloadFingerprint
1583
+ || replayRow.recovery_batch_id !== recoveryBatchId
1584
+ || replayRow.replay_of_client_id !== sourceClientId
1585
+ || replayRow.replay_of_operation_id !== sourceOperationId) {
1586
+ throw new WorkspaceSyncIdempotencyConflictError();
1587
+ }
1588
+ for (const { target, revision } of targetRevisions) {
1589
+ this.db.prepare(`
1590
+ INSERT INTO workspace_sync_outbox_targets (
1591
+ tenant_id, workspace_id, client_id, operation_id,
1592
+ domain, entity_id, mutation_kind,
1593
+ base_entity_revision, base_lifecycle_revision,
1594
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee, created_at
1595
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1596
+ ON CONFLICT (tenant_id, workspace_id, client_id, operation_id, domain, entity_id)
1597
+ DO NOTHING
1598
+ `).run(tenantId, workspaceId, sourceClientId, operationId, requireText(target.domain, 'Target domain'), requireText(target.entity_id, 'Target entity id'), target.domain === source.domain && target.entity_id === source.entity_id
1599
+ ? replayPayload.replayMutationKind
1600
+ : requireText(target.mutation_kind, 'Target mutation kind'), revision.entityRevision, revision.lifecycleRevision, Number(target.owns_task_metadata) === 1 ? 1 : 0, Number(target.owns_task_lifecycle) === 1 ? 1 : 0, Number(target.owns_task_assignee) === 1 ? 1 : 0, now);
1601
+ this.db.prepare(`
1602
+ UPDATE workspace_sync_outbox_targets
1603
+ SET base_entity_revision = ?, base_lifecycle_revision = ?
1604
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1605
+ AND domain = ? AND entity_id = ?
1606
+ `).run(revision.entityRevision, revision.lifecycleRevision, tenantId, workspaceId, sourceClientId, operationId, requireText(target.domain, 'Target domain'), requireText(target.entity_id, 'Target entity id'));
1607
+ }
1608
+ }
1609
+ const updated = this.db.prepare(`
1610
+ UPDATE workspace_sync_outbox_recoveries
1611
+ SET state = 'replay_queued', replay_queued_at = ?, updated_at = ?
1612
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1613
+ AND state = 'baseline_ready'
1614
+ `).run(now, now, tenantId, workspaceId, recoveryBatchId);
1615
+ if (Number(updated.changes || 0) !== 1) {
1616
+ throw new Error('Workspace sync recovery phase changed while preparing replay.');
1617
+ }
1618
+ return { recoveryBatchId, operationIds: listReplayOperationIds() };
1619
+ })();
1620
+ }
1621
+ readOpenOutboxRecovery(input) {
1622
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1623
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1624
+ const row = this.db.prepare(`
1625
+ SELECT recovery_batch_id, root_client_id, root_operation_id, state,
1626
+ baseline_marker, created_at, updated_at, completed_at
1627
+ FROM workspace_sync_outbox_recoveries
1628
+ WHERE tenant_id = ? AND workspace_id = ? AND state <> 'completed'
1629
+ ORDER BY created_at, recovery_batch_id
1630
+ LIMIT 1
1631
+ `).get(tenantId, workspaceId);
1632
+ return row ? this.mapOutboxRecovery(tenantId, workspaceId, row) : null;
1633
+ }
1634
+ readOutboxRecovery(input) {
1635
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1636
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1637
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1638
+ const row = this.db.prepare(`
1639
+ SELECT recovery_batch_id, root_client_id, root_operation_id, state,
1640
+ baseline_marker, created_at, updated_at, completed_at
1641
+ FROM workspace_sync_outbox_recoveries
1642
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1643
+ `).get(tenantId, workspaceId, recoveryBatchId);
1644
+ return row ? this.mapOutboxRecovery(tenantId, workspaceId, row) : null;
1645
+ }
1646
+ findOldestBlockedOutboxIdentity(input) {
1647
+ return this.listBlockedOutboxIdentities(input)[0] || null;
1648
+ }
1649
+ listBlockedOutboxIdentities(input) {
1650
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1651
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1652
+ const rows = this.db.prepare(`
1653
+ SELECT client_id, operation_id
1654
+ FROM workspace_sync_outbox
1655
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
1656
+ ORDER BY enqueue_sequence, created_at, operation_id
1657
+ `).all(tenantId, workspaceId);
1658
+ return rows.map((row) => ({
1659
+ tenantId,
1660
+ workspaceId,
1661
+ clientId: requireText(row.client_id, 'Client id'),
1662
+ operationId: requireText(row.operation_id, 'Operation id'),
1663
+ }));
1664
+ }
1665
+ restartOutboxRecoveryAfterRebaseConflict(input) {
1666
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1667
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1668
+ const recoveryBatchId = requireText(input.recoveryBatchId, 'Recovery batch id');
1669
+ const now = requireIsoTimestamp(input.now, 'Recovery restart time');
1670
+ return this.db.transaction(() => {
1671
+ const recovery = this.db.prepare(`
1672
+ SELECT state
1673
+ FROM workspace_sync_outbox_recoveries
1674
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1675
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1676
+ `).get(tenantId, workspaceId, recoveryBatchId);
1677
+ if (!recovery || recovery.state !== 'replay_queued')
1678
+ return false;
1679
+ const counts = this.db.prepare(`
1680
+ SELECT
1681
+ COUNT(*) AS total,
1682
+ SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) AS accepted,
1683
+ SUM(CASE WHEN status = 'in_flight' THEN 1 ELSE 0 END) AS in_flight,
1684
+ SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END) AS blocked,
1685
+ SUM(CASE
1686
+ WHEN status = 'blocked' AND last_error_code IN (
1687
+ 'WORKSPACE_SYNC_REVISION_CONFLICT',
1688
+ 'WORKSPACE_SYNC_IDEMPOTENCY_CONFLICT'
1689
+ )
1690
+ THEN 1 ELSE 0 END
1691
+ ) AS rebase_conflicts
1692
+ FROM workspace_sync_outbox
1693
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1694
+ AND replay_of_operation_id IS NOT NULL
1695
+ `).get(tenantId, workspaceId, recoveryBatchId);
1696
+ if (Number(counts.total || 0) === 0
1697
+ || Number(counts.accepted || 0) !== 0
1698
+ || Number(counts.in_flight || 0) !== 0) {
1699
+ return false;
1700
+ }
1701
+ const lifecycleConflicts = this.db.prepare(`
1702
+ SELECT source.entity_id, source.mutation_kind, source.payload_json,
1703
+ replay.last_error_code, replay.last_error_message
1704
+ FROM workspace_sync_outbox AS replay
1705
+ INNER JOIN workspace_sync_outbox AS source
1706
+ ON source.tenant_id = replay.tenant_id
1707
+ AND source.workspace_id = replay.workspace_id
1708
+ AND source.client_id = replay.replay_of_client_id
1709
+ AND source.operation_id = replay.replay_of_operation_id
1710
+ WHERE replay.tenant_id = ? AND replay.workspace_id = ?
1711
+ AND replay.recovery_batch_id = ?
1712
+ AND replay.status = 'blocked'
1713
+ AND replay.last_error_code IN (
1714
+ 'TASK_LIFECYCLE_ALREADY_ARCHIVED',
1715
+ 'TASK_LIFECYCLE_NOT_ARCHIVED',
1716
+ 'TASK_LIFECYCLE_STATE_CONFLICT'
1717
+ )
1718
+ `).all(tenantId, workspaceId, recoveryBatchId);
1719
+ const baselineSatisfied = lifecycleConflicts.filter((row) => (this.isTargetStateLifecycleConflict(row)
1720
+ && this.isTaskLifecycleRecoveryIntentSatisfied(tenantId, workspaceId, row)));
1721
+ if (Number(counts.rebase_conflicts || 0) === 0 && baselineSatisfied.length === 0) {
1722
+ return false;
1723
+ }
1724
+ if (Number(counts.blocked || 0)
1725
+ !== Number(counts.rebase_conflicts || 0) + baselineSatisfied.length) {
1726
+ return false;
1727
+ }
1728
+ this.db.prepare(`
1729
+ UPDATE workspace_sync_outbox
1730
+ SET status = 'superseded',
1731
+ lease_owner = NULL, lease_expires_at = NULL,
1732
+ superseded_at = ?, updated_at = ?
1733
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1734
+ AND replay_of_operation_id IS NOT NULL
1735
+ AND status IN ('pending', 'blocked')
1736
+ `).run(now, now, tenantId, workspaceId, recoveryBatchId);
1737
+ const restarted = this.db.prepare(`
1738
+ UPDATE workspace_sync_outbox_recoveries
1739
+ SET state = 'awaiting_baseline',
1740
+ baseline_marker = NULL, baseline_applied_at = NULL,
1741
+ replay_queued_at = NULL, updated_at = ?
1742
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1743
+ AND state = 'replay_queued'
1744
+ `).run(now, tenantId, workspaceId, recoveryBatchId);
1745
+ if (Number(restarted.changes || 0) !== 1) {
1746
+ throw new Error('Workspace sync recovery phase changed while restarting authoritative repair.');
1747
+ }
1748
+ return true;
1749
+ })();
1750
+ }
1751
+ completeOutboxRecoveryIfSettled(input) {
1752
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1753
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1754
+ const now = requireIsoTimestamp(input.now, 'Recovery completion time');
1755
+ return this.db.transaction(() => {
1756
+ let recovery = this.readOpenOutboxRecovery({ tenantId, workspaceId });
1757
+ let settledBaselineSatisfiedLegacyReplays = 0;
1758
+ if (recovery?.state === 'replay_queued' && recovery.baselineMarker) {
1759
+ const baselineSatisfiedLegacyReplays = this.db.prepare(`
1760
+ SELECT replay.client_id, replay.operation_id,
1761
+ source.dispatch_class, source.domain, source.entity_id,
1762
+ source.mutation_kind, source.payload_json, source.payload_fingerprint
1763
+ FROM workspace_sync_outbox AS replay
1764
+ INNER JOIN workspace_sync_outbox AS source
1765
+ ON source.tenant_id = replay.tenant_id
1766
+ AND source.workspace_id = replay.workspace_id
1767
+ AND source.client_id = replay.replay_of_client_id
1768
+ AND source.operation_id = replay.replay_of_operation_id
1769
+ WHERE replay.tenant_id = ? AND replay.workspace_id = ?
1770
+ AND replay.recovery_batch_id = ?
1771
+ AND replay.status = 'blocked'
1772
+ AND replay.dispatch_class = 'legacy-adoption'
1773
+ AND replay.domain = 'entity-event'
1774
+ AND replay.mutation_kind = 'adopt-legacy-batch'
1775
+ AND replay.last_error_code IN (
1776
+ 'WORKSPACE_SYNC_REVISION_CONFLICT',
1777
+ 'WORKSPACE_SYNC_IDEMPOTENCY_CONFLICT'
1778
+ )
1779
+ AND source.dispatch_class = 'legacy-adoption'
1780
+ AND source.domain = 'entity-event'
1781
+ AND source.mutation_kind = 'adopt-legacy-batch'
1782
+ AND source.recovery_batch_id = replay.recovery_batch_id
1783
+ AND source.status = 'superseded'
1784
+ AND source.replay_of_operation_id IS NULL
1785
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE OF replay' : ''}
1786
+ `).all(tenantId, workspaceId, recovery.recoveryBatchId);
1787
+ for (const replay of baselineSatisfiedLegacyReplays) {
1788
+ // Older builds replayed immutable legacy adoption after applying
1789
+ // the authoritative baseline. Validate the frozen source before
1790
+ // settling that stranded replay; all mutable/user-authored intents
1791
+ // continue through the ordinary rebase path.
1792
+ if (!isLegacyEntityEventAdoptionIdentity({
1793
+ clientId: requireText(replay.client_id, 'Replay client id'),
1794
+ operationId: requireText(replay.operation_id, 'Replay operation id'),
1795
+ })) {
1796
+ throw new Error('Legacy entity-event adoption recovery replay identity is invalid.');
1797
+ }
1798
+ this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, replay);
1799
+ const settled = this.db.prepare(`
1800
+ UPDATE workspace_sync_outbox
1801
+ SET status = 'superseded', superseded_at = ?, updated_at = ?,
1802
+ lease_owner = NULL, lease_expires_at = NULL
1803
+ WHERE tenant_id = ? AND workspace_id = ?
1804
+ AND client_id = ? AND operation_id = ?
1805
+ AND recovery_batch_id = ? AND status = 'blocked'
1806
+ `).run(now, now, tenantId, workspaceId, requireText(replay.client_id, 'Replay client id'), requireText(replay.operation_id, 'Replay operation id'), recovery.recoveryBatchId);
1807
+ if (Number(settled.changes || 0) !== 1) {
1808
+ throw new Error('Legacy entity-event adoption recovery replay changed while settling.');
1809
+ }
1810
+ settledBaselineSatisfiedLegacyReplays += 1;
1811
+ }
1812
+ if (baselineSatisfiedLegacyReplays.length > 0) {
1813
+ recovery = this.readOpenOutboxRecovery({ tenantId, workspaceId });
1814
+ }
1815
+ }
1816
+ if (!recovery || recovery.state !== 'replay_queued'
1817
+ || (recovery.replayTotal === 0 && settledBaselineSatisfiedLegacyReplays === 0)
1818
+ || recovery.replayAccepted !== recovery.replayTotal) {
1819
+ return recovery;
1820
+ }
1821
+ const updated = this.db.prepare(`
1822
+ UPDATE workspace_sync_outbox_recoveries
1823
+ SET state = 'completed', completed_at = ?, updated_at = ?
1824
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1825
+ AND state = 'replay_queued'
1826
+ `).run(now, now, tenantId, workspaceId, recovery.recoveryBatchId);
1827
+ if (Number(updated.changes || 0) !== 1) {
1828
+ throw new Error('Workspace sync recovery phase changed while recording completion.');
1829
+ }
1830
+ return this.readOutboxRecovery({ tenantId, workspaceId, recoveryBatchId: recovery.recoveryBatchId });
1831
+ })();
1832
+ }
1833
+ mapOutboxRecovery(tenantId, workspaceId, row) {
1834
+ const recoveryBatchId = requireText(row.recovery_batch_id, 'Recovery batch id');
1835
+ const counts = this.db.prepare(`
1836
+ SELECT status, COUNT(*) AS count
1837
+ FROM workspace_sync_outbox
1838
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
1839
+ AND replay_of_operation_id IS NOT NULL
1840
+ GROUP BY status
1841
+ `).all(tenantId, workspaceId, recoveryBatchId);
1842
+ const byStatus = new Map(counts.map((entry) => [String(entry.status || ''), Number(entry.count || 0)]));
1843
+ const replayPending = byStatus.get('pending') || 0;
1844
+ const replayInFlight = byStatus.get('in_flight') || 0;
1845
+ const replayBlocked = byStatus.get('blocked') || 0;
1846
+ const replayAccepted = byStatus.get('accepted') || 0;
1847
+ const replayTotal = replayPending + replayInFlight + replayBlocked + replayAccepted;
1848
+ const state = String(row.state || '');
1849
+ if (!['awaiting_baseline', 'baseline_ready', 'replay_queued', 'completed'].includes(state)) {
1850
+ throw new Error('Workspace sync recovery state is invalid.');
1851
+ }
1852
+ return {
1853
+ recoveryBatchId,
1854
+ rootClientId: requireText(row.root_client_id, 'Recovery root client id'),
1855
+ rootOperationId: requireText(row.root_operation_id, 'Recovery root operation id'),
1856
+ state,
1857
+ baselineMarker: typeof row.baseline_marker === 'string' ? row.baseline_marker : null,
1858
+ replayTotal,
1859
+ replayPending,
1860
+ replayInFlight,
1861
+ replayBlocked,
1862
+ replayAccepted,
1863
+ createdAt: requireIsoTimestamp(row.created_at, 'Recovery creation time'),
1864
+ updatedAt: requireIsoTimestamp(row.updated_at, 'Recovery update time'),
1865
+ completedAt: row.completed_at == null ? null : requireIsoTimestamp(row.completed_at, 'Recovery completion time'),
1866
+ };
1867
+ }
1868
+ readOperationStateForUpdate(identity) {
1869
+ const row = this.db.prepare(`
1870
+ SELECT status, response_json, created_at
1871
+ FROM workspace_sync_operation_results
1872
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1873
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
1874
+ `).get(...operationParams(identity));
1875
+ if (!row)
1876
+ return null;
1877
+ if (row.status !== 'processing' && row.status !== 'committed') {
1878
+ throw new Error(`Unsupported operation status: ${String(row.status || '')}`);
1879
+ }
1880
+ return {
1881
+ status: row.status,
1882
+ responseJson: typeof row.response_json === 'string' ? row.response_json : null,
1883
+ createdAt: requireIsoTimestamp(row.created_at, 'Operation creation time'),
1884
+ };
1885
+ }
1886
+ readCreateDependencyState(input) {
1887
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1888
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1889
+ const dependencies = Array.from(new Map(input.dependencies.map((dependency) => {
1890
+ const domain = requireText(dependency.domain, 'Dependency domain');
1891
+ const entityId = requireText(dependency.entityId, 'Dependency entity id');
1892
+ return [`${domain}\0${entityId}`, { domain, entityId }];
1893
+ })).values());
1894
+ if (dependencies.length === 0)
1895
+ return null;
1896
+ const clauses = dependencies.map(() => '(domain = ? AND entity_id = ?)').join(' OR ');
1897
+ const rows = this.db.prepare(`
1898
+ SELECT status
1899
+ FROM workspace_sync_outbox
1900
+ WHERE tenant_id = ? AND workspace_id = ?
1901
+ AND contract_version = ? AND mutation_kind = 'create'
1902
+ AND (${clauses})
1903
+ AND status IN ('pending', 'in_flight', 'blocked')
1904
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, ...dependencies.flatMap((dependency) => [dependency.domain, dependency.entityId]));
1905
+ if (rows.some((row) => row.status === 'blocked'))
1906
+ return 'blocked';
1907
+ return rows.length > 0 ? 'pending' : null;
1908
+ }
1909
+ readTaskCreateDependencyState(input) {
1910
+ return this.readCreateDependencyState({
1911
+ tenantId: input.tenantId,
1912
+ workspaceId: input.workspaceId,
1913
+ dependencies: input.taskIds.map((entityId) => ({ domain: 'task', entityId })),
1914
+ });
1915
+ }
1916
+ listActiveTaskRelationshipOperationIds(input) {
1917
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1918
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1919
+ const rows = this.db.prepare(`
1920
+ SELECT DISTINCT entity_id
1921
+ FROM workspace_sync_outbox
1922
+ WHERE tenant_id = ? AND workspace_id = ? AND contract_version = ?
1923
+ AND domain = 'task-relationship' AND mutation_kind IN ('link', 'unlink')
1924
+ AND status IN ('pending', 'in_flight', 'blocked')
1925
+ ORDER BY entity_id ASC
1926
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
1927
+ return rows.map((row) => requireText(row.entity_id, 'Task relationship id'));
1928
+ }
1929
+ listAttachmentAssetIdsAwaitingContentPublication(input) {
1930
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1931
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1932
+ const rows = this.db.prepare(`
1933
+ SELECT domain, payload_json
1934
+ FROM workspace_sync_outbox
1935
+ WHERE tenant_id = ? AND workspace_id = ? AND contract_version = ?
1936
+ AND (
1937
+ (domain = 'task-attachment-links' AND mutation_kind = 'replace')
1938
+ OR (domain = 'task' AND mutation_kind = 'create')
1939
+ )
1940
+ AND status IN ('pending', 'in_flight')
1941
+ AND last_error_code = 'TASK_ATTACHMENT_ASSET_NOT_READY'
1942
+ ORDER BY enqueue_sequence
1943
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
1944
+ const assetIds = new Set();
1945
+ for (const row of rows) {
1946
+ let payload;
1947
+ try {
1948
+ payload = JSON.parse(requireText(row.payload_json, 'Attachment-link payload'));
1949
+ }
1950
+ catch {
1951
+ continue;
1952
+ }
1953
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1954
+ continue;
1955
+ const links = String(row.domain || '') === 'task'
1956
+ ? payload.attachmentLinks
1957
+ : payload.links;
1958
+ if (!Array.isArray(links))
1959
+ continue;
1960
+ for (const link of links) {
1961
+ if (!link || typeof link !== 'object' || Array.isArray(link))
1962
+ continue;
1963
+ const assetId = String(link.assetId || '').trim();
1964
+ if (assetId)
1965
+ assetIds.add(assetId);
1966
+ }
1967
+ }
1968
+ return Array.from(assetIds).sort((a, b) => a.localeCompare(b));
1969
+ }
1970
+ runTaskRelationshipCoexistenceTransaction(input) {
1971
+ const tenantId = requireText(input.tenantId, 'Tenant id');
1972
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
1973
+ const now = requireIsoTimestamp(input.now, 'Coexistence lock timestamp');
1974
+ return this.db.transaction(() => {
1975
+ this.db.prepare(`
1976
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
1977
+ VALUES (?, ?, 'task-relationship', ?)
1978
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
1979
+ `).run(tenantId, workspaceId, now);
1980
+ return input.apply(this.listActiveTaskRelationshipOperationIds({ tenantId, workspaceId }));
1981
+ })();
1982
+ }
1983
+ reserveOperation(input) {
1984
+ const ids = operationParams(input.identity);
1985
+ const requestFingerprint = requireText(input.requestFingerprint, 'Request fingerprint');
1986
+ const now = requireIsoTimestamp(input.now, 'Reservation time');
1987
+ const leaseExpiresAt = requireIsoTimestamp(input.leaseExpiresAt, 'Lease expiry');
1988
+ if (leaseExpiresAt <= now)
1989
+ throw new Error('Lease expiry must be after the reservation time.');
1990
+ const leaseOwner = requireText(input.leaseOwner || randomUUID(), 'Lease owner');
1991
+ return this.db.transaction(() => {
1992
+ this.db.prepare(`
1993
+ INSERT INTO workspace_sync_operation_results (
1994
+ tenant_id, workspace_id, client_id, operation_id, contract_version,
1995
+ request_fingerprint, status, lease_owner, lease_expires_at,
1996
+ created_at, updated_at
1997
+ ) VALUES (?, ?, ?, ?, ?, ?, 'processing', ?, ?, ?, ?)
1998
+ ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
1999
+ `).run(...ids, WORKSPACE_SYNC_CONTRACT_VERSION, requestFingerprint, leaseOwner, leaseExpiresAt, now, now);
2000
+ const row = this.readOperationRow(ids);
2001
+ if (!row)
2002
+ throw new Error('Workspace sync operation reservation was not persisted.');
2003
+ this.assertMatchingOperation(row, requestFingerprint);
2004
+ if (row.status === 'committed') {
2005
+ if (typeof row.response_json !== 'string')
2006
+ throw new Error('Committed operation is missing its response.');
2007
+ return {
2008
+ state: 'committed',
2009
+ responseJson: row.response_json,
2010
+ acceptedSequence: row.accepted_sequence == null ? null : normalizeSequence(row.accepted_sequence),
2011
+ };
2012
+ }
2013
+ if (row.lease_owner === leaseOwner)
2014
+ return { state: 'owned', leaseOwner };
2015
+ const existingExpiry = requireIsoTimestamp(row.lease_expires_at, 'Existing lease expiry');
2016
+ if (existingExpiry > now)
2017
+ return { state: 'in_progress', retryAt: existingExpiry };
2018
+ const takeover = this.db.prepare(`
2019
+ UPDATE workspace_sync_operation_results
2020
+ SET lease_owner = ?, lease_expires_at = ?, updated_at = ?
2021
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2022
+ AND status = 'processing' AND request_fingerprint = ?
2023
+ AND lease_expires_at <= ?
2024
+ `).run(leaseOwner, leaseExpiresAt, now, ...ids, requestFingerprint, now);
2025
+ if (Number(takeover.changes || 0) === 1)
2026
+ return { state: 'owned', leaseOwner };
2027
+ const current = this.readOperationRow(ids);
2028
+ this.assertMatchingOperation(current, requestFingerprint);
2029
+ return {
2030
+ state: 'in_progress',
2031
+ retryAt: requireIsoTimestamp(current?.lease_expires_at, 'Existing lease expiry'),
2032
+ };
2033
+ })();
2034
+ }
2035
+ // A retryable failure (dependency not ready, revision conflict, etc.) can
2036
+ // be thrown after reserveOperation succeeds but before the operation
2037
+ // commits. Without this, the abandoned reservation blocks every retry
2038
+ // with WORKSPACE_SYNC_OPERATION_IN_PROGRESS until its lease naturally
2039
+ // expires, even though nothing was committed. This never deletes the row,
2040
+ // forces acceptance, or rewrites the operation identity; it only makes
2041
+ // the exact same identity immediately eligible for reservation takeover.
2042
+ releaseFailedOperationReservation(input) {
2043
+ const ids = operationParams(input.identity);
2044
+ const requestFingerprint = requireText(input.requestFingerprint, 'Request fingerprint');
2045
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
2046
+ const now = requireIsoTimestamp(input.now, 'Release time');
2047
+ this.db.prepare(`
2048
+ UPDATE workspace_sync_operation_results
2049
+ SET lease_expires_at = ?, updated_at = ?
2050
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2051
+ AND status = 'processing' AND request_fingerprint = ? AND lease_owner = ?
2052
+ `).run(now, now, ...ids, requestFingerprint, leaseOwner);
2053
+ }
2054
+ commitReservedOperationResponse(input) {
2055
+ const ids = operationParams(input.identity);
2056
+ const requestFingerprint = requireText(input.requestFingerprint, 'Request fingerprint');
2057
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
2058
+ const now = requireIsoTimestamp(input.now, 'Commit time');
2059
+ const responseJson = canonicalWorkspaceSyncJson(input.response);
2060
+ try {
2061
+ return this.db.transaction(() => {
2062
+ const committed = this.db.prepare(`
2063
+ UPDATE workspace_sync_operation_results
2064
+ SET status = 'committed', response_json = ?, accepted_sequence = NULL,
2065
+ lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
2066
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2067
+ AND contract_version = ? AND status = 'processing'
2068
+ AND request_fingerprint = ? AND lease_owner = ?
2069
+ `).run(responseJson, now, ...ids, WORKSPACE_SYNC_CONTRACT_VERSION, requestFingerprint, leaseOwner);
2070
+ if (Number(committed.changes || 0) !== 1)
2071
+ throw new WorkspaceSyncLeaseLostError();
2072
+ return { responseJson };
2073
+ })();
2074
+ }
2075
+ catch (error) {
2076
+ if (!(error instanceof WorkspaceSyncLeaseLostError)) {
2077
+ this.releaseFailedOperationReservation({ identity: input.identity, requestFingerprint, leaseOwner, now });
2078
+ }
2079
+ throw error;
2080
+ }
2081
+ }
2082
+ acceptReservedOperation(input) {
2083
+ return this.acceptReservedOperationBatch({
2084
+ identity: input.identity,
2085
+ requestFingerprint: input.requestFingerprint,
2086
+ leaseOwner: input.leaseOwner,
2087
+ now: input.now,
2088
+ entries: [{
2089
+ domain: input.domain,
2090
+ entityId: input.entityId,
2091
+ mutationKind: input.mutationKind,
2092
+ revisionKind: input.revisionKind,
2093
+ contentSha256: input.contentSha256,
2094
+ sizeBytes: input.sizeBytes,
2095
+ baseEntityRevision: input.baseEntityRevision,
2096
+ baseLifecycleRevision: input.baseLifecycleRevision,
2097
+ buildCanonicalFingerprint: input.buildCanonicalFingerprint,
2098
+ metadataOwned: input.metadataOwned,
2099
+ reopenAssigneeOwned: input.reopenAssigneeOwned,
2100
+ buildAuthoritativePayload: (result, revision) => input.buildAuthoritativePayload(result, {
2101
+ sequence: revision.sequence,
2102
+ entityRevision: revision.entityRevision,
2103
+ lifecycleRevision: revision.lifecycleRevision,
2104
+ }),
2105
+ }],
2106
+ applyMutation: input.applyMutation,
2107
+ buildResponse: (result, revisions) => input.buildResponse(result, {
2108
+ sequence: revisions[0].sequence,
2109
+ entityRevision: revisions[0].entityRevision,
2110
+ lifecycleRevision: revisions[0].lifecycleRevision,
2111
+ }),
2112
+ });
2113
+ }
2114
+ acceptReservedOperationBatch(input) {
2115
+ const ids = operationParams(input.identity);
2116
+ const requestFingerprint = requireText(input.requestFingerprint, 'Request fingerprint');
2117
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
2118
+ const now = requireIsoTimestamp(input.now, 'Acceptance time');
2119
+ try {
2120
+ if (!Array.isArray(input.entries) || input.entries.length === 0 || input.entries.length > 100) {
2121
+ throw new Error('Workspace sync journal entry count must be between 1 and 100.');
2122
+ }
2123
+ const entries = input.entries.map((entry) => ({
2124
+ ...entry,
2125
+ domain: requireText(entry.domain, 'Domain'),
2126
+ entityId: requireText(entry.entityId, 'Entity id'),
2127
+ mutationKind: requireText(entry.mutationKind, 'Mutation kind'),
2128
+ revisionKind: normalizeRevisionKind(entry.revisionKind),
2129
+ baseEntityRevision: normalizeSequence(entry.baseEntityRevision),
2130
+ baseLifecycleRevision: normalizeSequence(entry.baseLifecycleRevision),
2131
+ baseRevisionPolicy: entry.baseRevisionPolicy || 'exact',
2132
+ sizeBytes: entry.sizeBytes == null ? null : normalizeSequence(entry.sizeBytes),
2133
+ }));
2134
+ const targets = new Set();
2135
+ for (const entry of entries) {
2136
+ if (entry.baseRevisionPolicy !== 'exact' && entry.baseRevisionPolicy !== 'current') {
2137
+ throw new Error(`Unsupported workspace sync base revision policy: ${entry.baseRevisionPolicy}.`);
2138
+ }
2139
+ const target = `${entry.domain}\u0000${entry.entityId}`;
2140
+ if (targets.has(target))
2141
+ throw new Error(`Duplicate workspace sync journal target: ${entry.domain}/${entry.entityId}.`);
2142
+ targets.add(target);
2143
+ }
2144
+ return this.db.transaction(() => {
2145
+ const owned = this.db.prepare(`
2146
+ UPDATE workspace_sync_operation_results
2147
+ SET updated_at = ?
2148
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2149
+ AND contract_version = ? AND status = 'processing'
2150
+ AND request_fingerprint = ? AND lease_owner = ?
2151
+ `).run(now, ...ids, WORKSPACE_SYNC_CONTRACT_VERSION, requestFingerprint, leaseOwner);
2152
+ if (Number(owned.changes || 0) !== 1)
2153
+ throw new WorkspaceSyncLeaseLostError();
2154
+ // A zero-width allocation locks the workspace sequence row without
2155
+ // consuming a sequence. This lets a mutation determine whether its
2156
+ // optional journal entries exist while preserving a gap-free range.
2157
+ this.allocateSequenceRange(ids[0], ids[1], now, 0);
2158
+ const buildPendingRevision = (entry) => {
2159
+ const current = this.db.prepare(`
2160
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
2161
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision
2162
+ FROM workspace_sync_entity_revisions
2163
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
2164
+ `).get(ids[0], ids[1], entry.domain, entry.entityId);
2165
+ let entityRevision = BigInt(normalizeSequence(current?.entity_revision ?? 0));
2166
+ let lifecycleRevision = BigInt(normalizeSequence(current?.lifecycle_revision ?? 0));
2167
+ const baseEntityRevision = BigInt(entry.baseEntityRevision);
2168
+ const baseLifecycleRevision = BigInt(entry.baseLifecycleRevision);
2169
+ const invalidExact = entry.baseRevisionPolicy === 'exact'
2170
+ && (entityRevision !== baseEntityRevision || lifecycleRevision !== baseLifecycleRevision);
2171
+ const invalidCurrent = entry.baseRevisionPolicy === 'current'
2172
+ && (baseEntityRevision > entityRevision || baseLifecycleRevision > lifecycleRevision);
2173
+ if (invalidExact || invalidCurrent) {
2174
+ throw new WorkspaceSyncRevisionConflictError();
2175
+ }
2176
+ if (entry.revisionKind === 'metadata' || entry.revisionKind === 'both')
2177
+ entityRevision += 1n;
2178
+ if (entry.revisionKind === 'lifecycle' || entry.revisionKind === 'both')
2179
+ lifecycleRevision += 1n;
2180
+ // Earlier lifecycle-only writers could advance lifecycle past
2181
+ // entity revision. Preserve base-revision validation, then
2182
+ // repair the accepted coordinates before journaling new state.
2183
+ if (lifecycleRevision > entityRevision)
2184
+ entityRevision = lifecycleRevision;
2185
+ return {
2186
+ entityRevision: entityRevision.toString(10),
2187
+ lifecycleRevision: lifecycleRevision.toString(10),
2188
+ };
2189
+ };
2190
+ const requiredRevisions = new Map(entries
2191
+ .filter((entry) => !entry.include)
2192
+ .map((entry) => [`${entry.domain}\u0000${entry.entityId}`, buildPendingRevision(entry)]));
2193
+ const mutationResult = input.applyMutation();
2194
+ const activeEntries = entries.filter((entry) => entry.include?.(mutationResult) !== false);
2195
+ if (activeEntries.length === 0) {
2196
+ throw new Error('Workspace sync operation must produce at least one journal entry.');
2197
+ }
2198
+ const acceptedSequence = this.allocateSequenceRange(ids[0], ids[1], now, activeEntries.length);
2199
+ const firstSequence = BigInt(acceptedSequence) - BigInt(activeEntries.length) + 1n;
2200
+ const pendingRevisions = activeEntries.map((entry) => (requiredRevisions.get(`${entry.domain}\u0000${entry.entityId}`) || buildPendingRevision(entry)));
2201
+ const revisions = pendingRevisions.map((revision, index) => ({
2202
+ domain: activeEntries[index].domain,
2203
+ entityId: activeEntries[index].entityId,
2204
+ sequence: (firstSequence + BigInt(index)).toString(10),
2205
+ entityRevision: revision.entityRevision,
2206
+ lifecycleRevision: revision.lifecycleRevision,
2207
+ }));
2208
+ activeEntries.forEach((entry, index) => {
2209
+ const revision = revisions[index];
2210
+ const canonicalFingerprint = requireText(entry.buildCanonicalFingerprint(mutationResult, revision), 'Canonical fingerprint');
2211
+ const payloadJson = canonicalWorkspaceSyncJson(entry.buildAuthoritativePayload(mutationResult, revision));
2212
+ this.db.prepare(`
2213
+ INSERT INTO workspace_sync_entity_revisions (
2214
+ tenant_id, workspace_id, domain, entity_id, contract_version,
2215
+ entity_revision, lifecycle_revision, coexistence_owned, metadata_owned, reopen_assignee_owned, last_sequence,
2216
+ canonical_fingerprint, updated_at
2217
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
2218
+ ON CONFLICT (tenant_id, workspace_id, domain, entity_id) DO UPDATE SET
2219
+ contract_version = excluded.contract_version,
2220
+ entity_revision = excluded.entity_revision,
2221
+ lifecycle_revision = excluded.lifecycle_revision,
2222
+ coexistence_owned = 1,
2223
+ metadata_owned = CASE
2224
+ WHEN excluded.metadata_owned = 1 THEN 1
2225
+ ELSE workspace_sync_entity_revisions.metadata_owned
2226
+ END,
2227
+ reopen_assignee_owned = CASE
2228
+ WHEN excluded.reopen_assignee_owned = 1 THEN 1
2229
+ ELSE workspace_sync_entity_revisions.reopen_assignee_owned
2230
+ END,
2231
+ last_sequence = excluded.last_sequence,
2232
+ canonical_fingerprint = excluded.canonical_fingerprint,
2233
+ updated_at = excluded.updated_at
2234
+ `).run(ids[0], ids[1], entry.domain, entry.entityId, WORKSPACE_SYNC_CONTRACT_VERSION, revision.entityRevision, revision.lifecycleRevision, entry.domain === 'task' && (entry.mutationKind === 'metadata-update'
2235
+ || entry.mutationKind === 'create'
2236
+ || entry.metadataOwned?.(mutationResult) === true) ? 1 : 0, entry.domain === 'task' && entry.reopenAssigneeOwned?.(mutationResult) === true ? 1 : 0, revision.sequence, canonicalFingerprint, now);
2237
+ this.db.prepare(`
2238
+ INSERT INTO workspace_sync_journal (
2239
+ tenant_id, workspace_id, sequence, contract_version, operation_id,
2240
+ client_id, domain, entity_id, mutation_kind, entity_revision,
2241
+ lifecycle_revision, content_sha256, size_bytes, payload_json, occurred_at
2242
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2243
+ `).run(ids[0], ids[1], revision.sequence, WORKSPACE_SYNC_CONTRACT_VERSION, ids[3], ids[2], entry.domain, entry.entityId, entry.mutationKind, revision.entityRevision, revision.lifecycleRevision, entry.contentSha256 ?? null, entry.sizeBytes, payloadJson, now);
2244
+ });
2245
+ const responseJson = canonicalWorkspaceSyncJson(input.buildResponse(mutationResult, revisions));
2246
+ const committed = this.db.prepare(`
2247
+ UPDATE workspace_sync_operation_results
2248
+ SET status = 'committed', response_json = ?, accepted_sequence = ?,
2249
+ lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
2250
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2251
+ AND status = 'processing' AND request_fingerprint = ? AND lease_owner = ?
2252
+ `).run(responseJson, acceptedSequence, now, ...ids, requestFingerprint, leaseOwner);
2253
+ if (Number(committed.changes || 0) !== 1)
2254
+ throw new WorkspaceSyncLeaseLostError();
2255
+ return { responseJson, acceptedSequence };
2256
+ })();
2257
+ }
2258
+ catch (error) {
2259
+ if (!(error instanceof WorkspaceSyncLeaseLostError)) {
2260
+ this.releaseFailedOperationReservation({
2261
+ identity: input.identity,
2262
+ requestFingerprint,
2263
+ leaseOwner,
2264
+ now,
2265
+ });
2266
+ }
2267
+ throw error;
2268
+ }
2269
+ }
2270
+ enqueueLocalOperation(input) {
2271
+ const ids = operationParams(input.identity);
2272
+ const domain = requireText(input.domain, 'Domain');
2273
+ const entityId = requireText(input.entityId, 'Entity id');
2274
+ const mutationKind = requireText(input.mutationKind, 'Mutation kind');
2275
+ const dispatchClass = requireOutboxDispatchClass(input.dispatchClass ?? 'ordinary');
2276
+ const explicitEntityRevision = normalizeSequence(input.baseEntityRevision ?? 0);
2277
+ const explicitLifecycleRevision = normalizeSequence(input.baseLifecycleRevision ?? 0);
2278
+ let payloadJson = canonicalWorkspaceSyncJson(input.payload);
2279
+ let payloadFingerprint = fingerprintWorkspaceSyncPayload(input.payload);
2280
+ const now = requireIsoTimestamp(input.now, 'Enqueue time');
2281
+ const ownsTaskMetadata = domain === 'task' && (input.ownership?.taskMetadata ?? mutationKind === 'metadata-update');
2282
+ const ownsTaskLifecycle = domain === 'task' && (input.ownership?.taskLifecycle ?? ['archive', 'unarchive', 'complete', 'reopen'].includes(mutationKind));
2283
+ const ownsTaskAssignee = domain === 'task' && (input.ownership?.taskAssignee ?? mutationKind === 'reopen');
2284
+ const additionalTargets = (input.additionalTargets || []).map((target) => {
2285
+ const targetDomain = requireText(target.domain, 'Outbox target domain');
2286
+ return {
2287
+ domain: targetDomain,
2288
+ entityId: requireText(target.entityId, 'Outbox target entity id'),
2289
+ mutationKind: requireText(target.mutationKind, 'Outbox target mutation kind'),
2290
+ baseEntityRevision: normalizeSequence(target.baseEntityRevision),
2291
+ baseLifecycleRevision: normalizeSequence(target.baseLifecycleRevision),
2292
+ ownsTaskMetadata: targetDomain === 'task' && target.ownsTaskMetadata === true,
2293
+ ownsTaskLifecycle: targetDomain === 'task' && target.ownsTaskLifecycle === true,
2294
+ ownsTaskAssignee: targetDomain === 'task' && target.ownsTaskAssignee === true,
2295
+ };
2296
+ }).sort((left, right) => (compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.entityId, right.entityId)));
2297
+ if (additionalTargets.length > 0 && input.revisionSource !== 'current') {
2298
+ throw new Error('Multi-target local operations must capture current revisions atomically.');
2299
+ }
2300
+ if ((input.requiredCurrentRevision || input.validateLockedState) && input.revisionSource !== 'current') {
2301
+ throw new Error('Locked local enqueue preconditions require current revision capture.');
2302
+ }
2303
+ const queuedMetadataSuccessorDomain = input.queuedMetadataSuccessorDomain;
2304
+ if (queuedMetadataSuccessorDomain && (input.revisionSource !== 'current'
2305
+ || domain !== queuedMetadataSuccessorDomain
2306
+ || mutationKind !== 'metadata-update'
2307
+ || additionalTargets.length > 0
2308
+ || (domain === 'task' && (!ownsTaskMetadata || ownsTaskLifecycle || ownsTaskAssignee)))) {
2309
+ throw new Error('Queued successors are supported only for matching single-target metadata updates.');
2310
+ }
2311
+ const queuedTaskSuccessor = input.queuedTaskSuccessor === true;
2312
+ const queuedWorkflowRuntimeSuccessor = input.queuedWorkflowRuntimeSuccessor === true;
2313
+ const workflowTaskTarget = additionalTargets[0];
2314
+ if (queuedWorkflowRuntimeSuccessor && (input.revisionSource !== 'current'
2315
+ || domain !== 'workflow-execution'
2316
+ || !['transition', 'complete-step'].includes(mutationKind)
2317
+ || additionalTargets.length > 1
2318
+ || (additionalTargets.length === 1 && (workflowTaskTarget.domain !== 'task'
2319
+ || workflowTaskTarget.mutationKind !== 'metadata-update'
2320
+ || !workflowTaskTarget.ownsTaskMetadata
2321
+ || workflowTaskTarget.ownsTaskLifecycle
2322
+ || workflowTaskTarget.ownsTaskAssignee))
2323
+ || ownsTaskMetadata || ownsTaskLifecycle || ownsTaskAssignee)) {
2324
+ throw new Error('Queued workflow runtime successors require a workflow execution root and optional task metadata target.');
2325
+ }
2326
+ const coalesceWorkflowAssignmentExecutionId = String(input.coalesceWorkflowAssignmentExecutionId || '').trim();
2327
+ if (coalesceWorkflowAssignmentExecutionId && (input.revisionSource !== 'current'
2328
+ || domain !== 'task'
2329
+ || mutationKind !== 'set-status'
2330
+ || !queuedTaskSuccessor)) {
2331
+ throw new Error('Workflow assignment dependency coalescing requires a queued task status operation.');
2332
+ }
2333
+ const coalescedWorkflowPayloadFingerprint = coalesceWorkflowAssignmentExecutionId
2334
+ ? (() => {
2335
+ const normalized = JSON.parse(payloadJson);
2336
+ delete normalized.workflowAssignment;
2337
+ return fingerprintWorkspaceSyncPayload(normalized);
2338
+ })()
2339
+ : null;
2340
+ const taskAggregateRootTarget = additionalTargets[0];
2341
+ const queueableTaskRoot = domain === 'task'
2342
+ && TASK_ROOT_SUCCESSOR_MUTATIONS.has(mutationKind)
2343
+ && additionalTargets.length === 0;
2344
+ const queueableTaskAggregate = Boolean(TASK_AGGREGATE_SUCCESSOR_MUTATIONS.get(domain)?.has(mutationKind))
2345
+ && ((domain === 'task-comments' && mutationKind === 'reconcile' && additionalTargets.length === 0)
2346
+ || (additionalTargets.length === 1
2347
+ && taskAggregateRootTarget.domain === 'task'
2348
+ && taskAggregateRootTarget.entityId === entityId
2349
+ && taskAggregateRootTarget.mutationKind === 'metadata-update'
2350
+ && taskAggregateRootTarget.ownsTaskMetadata
2351
+ && !taskAggregateRootTarget.ownsTaskLifecycle
2352
+ && !taskAggregateRootTarget.ownsTaskAssignee))
2353
+ && !ownsTaskMetadata && !ownsTaskLifecycle && !ownsTaskAssignee;
2354
+ if (queuedTaskSuccessor && (input.revisionSource !== 'current' || (!queueableTaskRoot && !queueableTaskAggregate))) {
2355
+ throw new Error('Queued task successors require a recognized task root or aggregate operation.');
2356
+ }
2357
+ const additionalTargetKeys = new Set();
2358
+ for (const target of additionalTargets) {
2359
+ const key = `${target.domain}\u0000${target.entityId}`;
2360
+ if (key === `${domain}\u0000${entityId}` || additionalTargetKeys.has(key)) {
2361
+ throw new Error(`Duplicate workspace sync outbox target: ${target.domain}/${target.entityId}.`);
2362
+ }
2363
+ additionalTargetKeys.add(key);
2364
+ }
2365
+ return this.db.transaction(() => {
2366
+ const lock = this.db.prepare(`
2367
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
2368
+ VALUES (?, ?, ?, ?)
2369
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
2370
+ `);
2371
+ if (requiresInitiativeProjectionLock([{ domain, entityId }, ...additionalTargets])) {
2372
+ lock.run(ids[0], ids[1], WORKSPACE_SYNC_INITIATIVE_PROJECTION_LOCK, now);
2373
+ }
2374
+ if (input.requireV3PlanningOwnership === 'combined'
2375
+ || requiresCombinedProjectionLock([{ domain, entityId }, ...additionalTargets])) {
2376
+ lock.run(ids[0], ids[1], WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
2377
+ }
2378
+ if (input.requireV3PlanningOwnership) {
2379
+ const ownership = input.requireV3PlanningOwnership === 'initiative'
2380
+ ? this.readInitiativeProjectionOwnership({ tenantId: ids[0], workspaceId: ids[1] })
2381
+ : this.readCombinedProjectionOwnership({ tenantId: ids[0], workspaceId: ids[1] });
2382
+ if (ownership.owner !== 'v3')
2383
+ throw new WorkspaceSyncPlanningOwnershipChangedError();
2384
+ }
2385
+ if (input.revisionSource === 'current') {
2386
+ const targets = normalizeWorkspaceSyncLockTargets([
2387
+ { domain, entityId },
2388
+ ...additionalTargets.map((target) => ({ domain: target.domain, entityId: target.entityId })),
2389
+ ]);
2390
+ for (const target of targets) {
2391
+ lock.run(ids[0], ids[1], `local-outbox:${target.domain}:${target.entityId}`, now);
2392
+ }
2393
+ if (input.requiredCurrentRevision) {
2394
+ const revision = this.db.prepare(`
2395
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
2396
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision
2397
+ FROM workspace_sync_entity_revisions
2398
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
2399
+ `).get(ids[0], ids[1], domain, entityId);
2400
+ if (normalizeSequence(revision?.entity_revision ?? 0)
2401
+ !== normalizeSequence(input.requiredCurrentRevision.entityRevision)
2402
+ || normalizeSequence(revision?.lifecycle_revision ?? 0)
2403
+ !== normalizeSequence(input.requiredCurrentRevision.lifecycleRevision))
2404
+ throw new WorkspaceSyncRevisionConflictError();
2405
+ }
2406
+ input.validateLockedState?.();
2407
+ const prior = this.db.prepare(`
2408
+ SELECT contract_version, dispatch_class, domain, entity_id, mutation_kind, payload_fingerprint,
2409
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee
2410
+ FROM workspace_sync_outbox
2411
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2412
+ `).get(...ids);
2413
+ if (prior) {
2414
+ const matches = Number(prior.contract_version) === WORKSPACE_SYNC_CONTRACT_VERSION
2415
+ && prior.dispatch_class === dispatchClass
2416
+ && prior.domain === domain
2417
+ && prior.entity_id === entityId
2418
+ && prior.mutation_kind === mutationKind
2419
+ && Number(prior.owns_task_metadata) === (ownsTaskMetadata ? 1 : 0)
2420
+ && Number(prior.owns_task_lifecycle) === (ownsTaskLifecycle ? 1 : 0)
2421
+ && Number(prior.owns_task_assignee) === (ownsTaskAssignee ? 1 : 0)
2422
+ && (prior.payload_fingerprint === payloadFingerprint
2423
+ || prior.payload_fingerprint === coalescedWorkflowPayloadFingerprint);
2424
+ const persistedTargets = this.db.prepare(`
2425
+ SELECT domain, entity_id, mutation_kind,
2426
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
2427
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
2428
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee
2429
+ FROM workspace_sync_outbox_targets
2430
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2431
+ AND NOT (domain = ? AND entity_id = ?)
2432
+ ORDER BY domain, entity_id
2433
+ `).all(...ids, domain, entityId);
2434
+ const targetsMatch = persistedTargets.length === additionalTargets.length
2435
+ && persistedTargets.every((persisted, index) => {
2436
+ const expected = additionalTargets[index];
2437
+ return persisted.domain === expected.domain
2438
+ && persisted.entity_id === expected.entityId
2439
+ && persisted.mutation_kind === expected.mutationKind
2440
+ && normalizeSequence(persisted.base_entity_revision) === expected.baseEntityRevision
2441
+ && normalizeSequence(persisted.base_lifecycle_revision) === expected.baseLifecycleRevision
2442
+ && Number(persisted.owns_task_metadata) === (expected.ownsTaskMetadata ? 1 : 0)
2443
+ && Number(persisted.owns_task_lifecycle) === (expected.ownsTaskLifecycle ? 1 : 0)
2444
+ && Number(persisted.owns_task_assignee) === (expected.ownsTaskAssignee ? 1 : 0);
2445
+ });
2446
+ if (!matches || !targetsMatch)
2447
+ throw new WorkspaceSyncIdempotencyConflictError();
2448
+ return { enqueued: false };
2449
+ }
2450
+ }
2451
+ const current = input.revisionSource === 'current'
2452
+ ? this.db.prepare(`
2453
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
2454
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision
2455
+ FROM workspace_sync_entity_revisions
2456
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
2457
+ `).get(ids[0], ids[1], domain, entityId)
2458
+ : undefined;
2459
+ const baseEntityRevision = input.revisionSource === 'current'
2460
+ ? normalizeSequence(current?.entity_revision ?? 0)
2461
+ : explicitEntityRevision;
2462
+ const baseLifecycleRevision = input.revisionSource === 'current'
2463
+ ? normalizeSequence(current?.lifecycle_revision ?? 0)
2464
+ : explicitLifecycleRevision;
2465
+ let queuedBehindActiveOperation = false;
2466
+ if (input.revisionSource === 'current') {
2467
+ const targets = [{ domain, entityId }, ...additionalTargets];
2468
+ for (const target of targets) {
2469
+ const active = this.db.prepare(`
2470
+ SELECT outbox.operation_id, outbox.domain, outbox.entity_id, outbox.mutation_kind,
2471
+ outbox.status, outbox.attempt_count, outbox.next_attempt_at,
2472
+ outbox.lease_expires_at, outbox.last_error_code,
2473
+ outbox.created_at, outbox.updated_at,
2474
+ outbox.payload_json,
2475
+ outbox.owns_task_metadata, outbox.owns_task_lifecycle,
2476
+ outbox.owns_task_assignee,
2477
+ (SELECT COUNT(*) FROM workspace_sync_outbox_targets AS counted_target
2478
+ WHERE counted_target.tenant_id = outbox.tenant_id
2479
+ AND counted_target.workspace_id = outbox.workspace_id
2480
+ AND counted_target.client_id = outbox.client_id
2481
+ AND counted_target.operation_id = outbox.operation_id) AS target_count,
2482
+ (SELECT COUNT(*) FROM workspace_sync_outbox_targets AS aggregate_target
2483
+ WHERE aggregate_target.tenant_id = outbox.tenant_id
2484
+ AND aggregate_target.workspace_id = outbox.workspace_id
2485
+ AND aggregate_target.client_id = outbox.client_id
2486
+ AND aggregate_target.operation_id = outbox.operation_id
2487
+ AND (
2488
+ (aggregate_target.domain = outbox.domain
2489
+ AND aggregate_target.entity_id = outbox.entity_id
2490
+ AND aggregate_target.mutation_kind = outbox.mutation_kind
2491
+ AND aggregate_target.owns_task_metadata = 0
2492
+ AND aggregate_target.owns_task_lifecycle = 0
2493
+ AND aggregate_target.owns_task_assignee = 0)
2494
+ OR (aggregate_target.domain = 'task'
2495
+ AND aggregate_target.entity_id = outbox.entity_id
2496
+ AND aggregate_target.mutation_kind = 'metadata-update'
2497
+ AND aggregate_target.owns_task_metadata = 1
2498
+ AND aggregate_target.owns_task_lifecycle = 0
2499
+ AND aggregate_target.owns_task_assignee = 0)
2500
+ )) AS task_aggregate_target_count
2501
+ FROM workspace_sync_outbox AS outbox
2502
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
2503
+ AND outbox.status IN ('pending', 'in_flight', 'blocked')
2504
+ AND NOT (outbox.client_id = ? AND outbox.operation_id = ?)
2505
+ AND (
2506
+ (outbox.domain = ? AND outbox.entity_id = ?)
2507
+ OR EXISTS (
2508
+ SELECT 1 FROM workspace_sync_outbox_targets AS persisted_target
2509
+ WHERE persisted_target.tenant_id = outbox.tenant_id
2510
+ AND persisted_target.workspace_id = outbox.workspace_id
2511
+ AND persisted_target.client_id = outbox.client_id
2512
+ AND persisted_target.operation_id = outbox.operation_id
2513
+ AND persisted_target.domain = ? AND persisted_target.entity_id = ?
2514
+ )
2515
+ )
2516
+ ORDER BY CASE outbox.status
2517
+ WHEN 'blocked' THEN 0
2518
+ WHEN 'in_flight' THEN 1
2519
+ ELSE 2
2520
+ END,
2521
+ outbox.enqueue_sequence
2522
+ `).all(ids[0], ids[1], ids[2], ids[3], target.domain, target.entityId, target.domain, target.entityId);
2523
+ if (active.length > 0) {
2524
+ const isCompatibleTaskAggregate = (row) => {
2525
+ const activeDomain = String(row.domain || '');
2526
+ const activeMutationKind = String(row.mutation_kind || '');
2527
+ const isReconciliation = activeDomain === 'task-comments'
2528
+ && activeMutationKind === 'reconcile'
2529
+ && Number(row.target_count) === 1;
2530
+ return row.entity_id === entityId
2531
+ && Boolean(TASK_AGGREGATE_SUCCESSOR_MUTATIONS.get(activeDomain)
2532
+ ?.has(activeMutationKind))
2533
+ && Number(row.owns_task_metadata) === 0
2534
+ && Number(row.owns_task_lifecycle) === 0
2535
+ && Number(row.owns_task_assignee) === 0
2536
+ && (isReconciliation || (Number(row.target_count) === 2
2537
+ && Number(row.task_aggregate_target_count) === 2));
2538
+ };
2539
+ const isCompatibleWorkflowRuntimeAggregate = (row) => (row.domain === 'workflow-execution'
2540
+ && row.mutation_kind === 'complete-step'
2541
+ && Number(row.owns_task_metadata) === 0
2542
+ && Number(row.owns_task_lifecycle) === 0
2543
+ && Number(row.owns_task_assignee) === 0
2544
+ && Number(row.target_count) === 2);
2545
+ const compatibleMetadataSuccessor = Boolean(queuedMetadataSuccessorDomain)
2546
+ && target.domain === domain
2547
+ && target.entityId === entityId
2548
+ && active.every((row) => ((row.domain === queuedMetadataSuccessorDomain
2549
+ && row.entity_id === entityId
2550
+ && row.mutation_kind === 'metadata-update'
2551
+ && (row.domain !== 'task' || (Number(row.owns_task_metadata) === 1
2552
+ && Number(row.owns_task_lifecycle) === 0
2553
+ && Number(row.owns_task_assignee) === 0))
2554
+ && Number(row.target_count) === 1)
2555
+ || (queuedMetadataSuccessorDomain === 'task'
2556
+ && (isCompatibleTaskAggregate(row)
2557
+ || isCompatibleWorkflowRuntimeAggregate(row)))));
2558
+ const compatibleTaskSuccessor = queuedTaskSuccessor
2559
+ && target.entityId === entityId
2560
+ && active.every((row) => {
2561
+ const activeDomain = String(row.domain || '');
2562
+ const activeMutationKind = String(row.mutation_kind || '');
2563
+ const isTaskRoot = activeDomain === 'task'
2564
+ && row.entity_id === entityId
2565
+ && (TASK_ROOT_SUCCESSOR_MUTATIONS.has(activeMutationKind)
2566
+ || (queueableTaskAggregate && activeMutationKind === 'create'))
2567
+ && Number(row.target_count) === 1;
2568
+ return isTaskRoot
2569
+ || isCompatibleTaskAggregate(row)
2570
+ || isCompatibleWorkflowRuntimeAggregate(row);
2571
+ });
2572
+ const compatibleWorkflowRuntimeSuccessor = queuedWorkflowRuntimeSuccessor
2573
+ && active.every((row) => (row.domain === 'workflow-execution'
2574
+ && row.entity_id === entityId
2575
+ && ['transition', 'complete-step'].includes(String(row.mutation_kind || ''))
2576
+ && Number(row.owns_task_metadata) === 0
2577
+ && Number(row.owns_task_lifecycle) === 0
2578
+ && Number(row.owns_task_assignee) === 0
2579
+ && [1, 2].includes(Number(row.target_count))));
2580
+ if (!compatibleMetadataSuccessor
2581
+ && !compatibleTaskSuccessor
2582
+ && !compatibleWorkflowRuntimeSuccessor) {
2583
+ throw workspaceSyncEntityBusyError({ target, active, now });
2584
+ }
2585
+ queuedBehindActiveOperation = true;
2586
+ if (coalesceWorkflowAssignmentExecutionId
2587
+ && target.domain === 'task' && target.entityId === entityId) {
2588
+ const dependencyAlreadyQueued = active.some((row) => {
2589
+ try {
2590
+ const activePayload = JSON.parse(String(row.payload_json || ''));
2591
+ return String(activePayload.workflowAssignment?.execution?.id || '')
2592
+ === coalesceWorkflowAssignmentExecutionId;
2593
+ }
2594
+ catch {
2595
+ return false;
2596
+ }
2597
+ });
2598
+ if (dependencyAlreadyQueued) {
2599
+ const normalizedPayload = JSON.parse(payloadJson);
2600
+ delete normalizedPayload.workflowAssignment;
2601
+ payloadJson = canonicalWorkspaceSyncJson(normalizedPayload);
2602
+ payloadFingerprint = fingerprintWorkspaceSyncPayload(normalizedPayload);
2603
+ }
2604
+ }
2605
+ }
2606
+ }
2607
+ for (const target of additionalTargets) {
2608
+ const revision = this.db.prepare(`
2609
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
2610
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision
2611
+ FROM workspace_sync_entity_revisions
2612
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
2613
+ `).get(ids[0], ids[1], target.domain, target.entityId);
2614
+ if (normalizeSequence(revision?.entity_revision ?? 0) !== target.baseEntityRevision
2615
+ || normalizeSequence(revision?.lifecycle_revision ?? 0) !== target.baseLifecycleRevision)
2616
+ throw new WorkspaceSyncRevisionConflictError();
2617
+ }
2618
+ }
2619
+ const enqueueSequence = this.allocateOutboxEnqueueSequence(ids[0], ids[1], now);
2620
+ const inserted = this.db.prepare(`
2621
+ INSERT INTO workspace_sync_outbox (
2622
+ tenant_id, workspace_id, operation_id, contract_version, client_id,
2623
+ dispatch_class, domain, entity_id, mutation_kind, owns_task_metadata,
2624
+ owns_task_lifecycle, owns_task_assignee, base_entity_revision,
2625
+ base_lifecycle_revision, payload_json, payload_fingerprint,
2626
+ status, attempt_count, enqueue_sequence, rebase_before_first_attempt,
2627
+ next_attempt_at, created_at, updated_at
2628
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?, ?, ?, ?)
2629
+ ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
2630
+ `).run(ids[0], ids[1], ids[3], WORKSPACE_SYNC_CONTRACT_VERSION, ids[2], dispatchClass, domain, entityId, mutationKind, ownsTaskMetadata ? 1 : 0, ownsTaskLifecycle ? 1 : 0, ownsTaskAssignee ? 1 : 0, baseEntityRevision, baseLifecycleRevision, payloadJson, payloadFingerprint, enqueueSequence, queuedBehindActiveOperation ? 1 : 0, now, now, now);
2631
+ if (Number(inserted.changes || 0) === 1) {
2632
+ const insertTarget = this.db.prepare(`
2633
+ INSERT INTO workspace_sync_outbox_targets (
2634
+ tenant_id, workspace_id, client_id, operation_id, domain, entity_id,
2635
+ mutation_kind, base_entity_revision, base_lifecycle_revision,
2636
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee, created_at
2637
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2638
+ `);
2639
+ const targets = [{
2640
+ domain, entityId, mutationKind, baseEntityRevision, baseLifecycleRevision,
2641
+ ownsTaskMetadata, ownsTaskLifecycle, ownsTaskAssignee,
2642
+ }, ...additionalTargets];
2643
+ for (const target of targets) {
2644
+ insertTarget.run(ids[0], ids[1], ids[2], ids[3], target.domain, target.entityId, target.mutationKind, target.baseEntityRevision, target.baseLifecycleRevision, target.ownsTaskMetadata ? 1 : 0, target.ownsTaskLifecycle ? 1 : 0, target.ownsTaskAssignee ? 1 : 0, now);
2645
+ }
2646
+ return { enqueued: true, mutationResult: input.applyLocalMutation() };
2647
+ }
2648
+ const existing = this.db.prepare(`
2649
+ SELECT contract_version, dispatch_class, domain, entity_id, mutation_kind,
2650
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
2651
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
2652
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
2653
+ payload_fingerprint
2654
+ FROM workspace_sync_outbox
2655
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2656
+ `).get(...ids);
2657
+ const matches = existing
2658
+ && Number(existing.contract_version) === WORKSPACE_SYNC_CONTRACT_VERSION
2659
+ && existing.dispatch_class === dispatchClass
2660
+ && existing.domain === domain
2661
+ && existing.entity_id === entityId
2662
+ && existing.mutation_kind === mutationKind
2663
+ && Number(existing.owns_task_metadata) === (ownsTaskMetadata ? 1 : 0)
2664
+ && Number(existing.owns_task_lifecycle) === (ownsTaskLifecycle ? 1 : 0)
2665
+ && Number(existing.owns_task_assignee) === (ownsTaskAssignee ? 1 : 0)
2666
+ && normalizeSequence(existing.base_entity_revision) === baseEntityRevision
2667
+ && normalizeSequence(existing.base_lifecycle_revision) === baseLifecycleRevision
2668
+ && (existing.payload_fingerprint === payloadFingerprint
2669
+ || existing.payload_fingerprint === coalescedWorkflowPayloadFingerprint);
2670
+ if (!matches)
2671
+ throw new WorkspaceSyncIdempotencyConflictError();
2672
+ return { enqueued: false };
2673
+ })();
2674
+ }
2675
+ listTaskMetadataOwnership(input) {
2676
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2677
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2678
+ const rows = this.db.prepare(`
2679
+ WITH ownership_sources AS (
2680
+ SELECT entity_id,
2681
+ 0 AS active_operation,
2682
+ 0 AS active_create_operation,
2683
+ 0 AS active_delete_operation,
2684
+ CASE WHEN EXISTS (
2685
+ SELECT 1 FROM workspace_sync_journal AS delete_journal
2686
+ WHERE delete_journal.tenant_id = workspace_sync_entity_revisions.tenant_id
2687
+ AND delete_journal.workspace_id = workspace_sync_entity_revisions.workspace_id
2688
+ AND delete_journal.domain = 'task'
2689
+ AND delete_journal.entity_id = workspace_sync_entity_revisions.entity_id
2690
+ AND delete_journal.mutation_kind = 'delete'
2691
+ AND delete_journal.sequence = workspace_sync_entity_revisions.last_sequence
2692
+ ) THEN 1 ELSE 0 END AS delete_owned,
2693
+ metadata_owned,
2694
+ 0 AS active_lifecycle_operation,
2695
+ 0 AS active_reopen_operation,
2696
+ reopen_assignee_owned,
2697
+ CAST(entity_revision AS TEXT) AS entity_revision,
2698
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision,
2699
+ CAST(last_sequence AS TEXT) AS last_sequence
2700
+ FROM workspace_sync_entity_revisions
2701
+ WHERE tenant_id = ? AND workspace_id = ?
2702
+ AND contract_version = ? AND domain = 'task'
2703
+ AND coexistence_owned = 1
2704
+ AND (metadata_owned = 1 OR lifecycle_revision > 0)
2705
+ UNION ALL
2706
+ SELECT target.entity_id,
2707
+ CASE WHEN target.owns_task_metadata = 1 OR target.mutation_kind = 'metadata-update' THEN 1 ELSE 0 END AS active_operation,
2708
+ CASE WHEN target.mutation_kind = 'create' THEN 1 ELSE 0 END AS active_create_operation,
2709
+ CASE WHEN target.mutation_kind = 'delete' THEN 1 ELSE 0 END AS active_delete_operation,
2710
+ CASE WHEN target.mutation_kind = 'delete' THEN 1 ELSE 0 END AS delete_owned,
2711
+ COALESCE(revisions.metadata_owned, 0) AS metadata_owned,
2712
+ CASE WHEN target.owns_task_lifecycle = 1 OR target.mutation_kind IN ('archive', 'unarchive', 'complete', 'reopen') THEN 1 ELSE 0 END AS active_lifecycle_operation,
2713
+ CASE WHEN target.owns_task_assignee = 1 OR target.mutation_kind = 'reopen' THEN 1 ELSE 0 END AS active_reopen_operation,
2714
+ 0 AS reopen_assignee_owned,
2715
+ CAST(revisions.entity_revision AS TEXT) AS entity_revision,
2716
+ CAST(revisions.lifecycle_revision AS TEXT) AS lifecycle_revision,
2717
+ CAST(revisions.last_sequence AS TEXT) AS last_sequence
2718
+ FROM workspace_sync_outbox_targets AS target
2719
+ INNER JOIN workspace_sync_outbox AS outbox
2720
+ ON outbox.tenant_id = target.tenant_id
2721
+ AND outbox.workspace_id = target.workspace_id
2722
+ AND outbox.client_id = target.client_id
2723
+ AND outbox.operation_id = target.operation_id
2724
+ LEFT JOIN workspace_sync_entity_revisions AS revisions
2725
+ ON revisions.tenant_id = target.tenant_id
2726
+ AND revisions.workspace_id = target.workspace_id
2727
+ AND revisions.contract_version = outbox.contract_version
2728
+ AND revisions.domain = target.domain
2729
+ AND revisions.entity_id = target.entity_id
2730
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
2731
+ AND outbox.contract_version = ? AND target.domain = 'task'
2732
+ AND (target.mutation_kind = 'create'
2733
+ OR target.owns_task_metadata = 1
2734
+ OR target.owns_task_lifecycle = 1
2735
+ OR target.owns_task_assignee = 1
2736
+ OR target.mutation_kind IN ('metadata-update', 'delete', 'archive', 'unarchive', 'complete', 'reopen'))
2737
+ AND (
2738
+ outbox.status IN ('pending', 'in_flight', 'blocked')
2739
+ OR (target.mutation_kind = 'delete' AND outbox.status = 'accepted')
2740
+ )
2741
+ UNION ALL
2742
+ SELECT outbox.entity_id,
2743
+ CASE WHEN outbox.owns_task_metadata = 1 OR outbox.mutation_kind = 'metadata-update' THEN 1 ELSE 0 END AS active_operation,
2744
+ CASE WHEN outbox.mutation_kind = 'create' THEN 1 ELSE 0 END AS active_create_operation,
2745
+ CASE WHEN outbox.mutation_kind = 'delete' THEN 1 ELSE 0 END AS active_delete_operation,
2746
+ CASE WHEN outbox.mutation_kind = 'delete' THEN 1 ELSE 0 END AS delete_owned,
2747
+ COALESCE(revisions.metadata_owned, 0) AS metadata_owned,
2748
+ CASE WHEN outbox.owns_task_lifecycle = 1 OR outbox.mutation_kind IN ('archive', 'unarchive', 'complete', 'reopen') THEN 1 ELSE 0 END AS active_lifecycle_operation,
2749
+ CASE WHEN outbox.owns_task_assignee = 1 OR outbox.mutation_kind = 'reopen' THEN 1 ELSE 0 END AS active_reopen_operation,
2750
+ 0 AS reopen_assignee_owned,
2751
+ CAST(revisions.entity_revision AS TEXT) AS entity_revision,
2752
+ CAST(revisions.lifecycle_revision AS TEXT) AS lifecycle_revision,
2753
+ CAST(revisions.last_sequence AS TEXT) AS last_sequence
2754
+ FROM workspace_sync_outbox AS outbox
2755
+ LEFT JOIN workspace_sync_entity_revisions AS revisions
2756
+ ON revisions.tenant_id = outbox.tenant_id
2757
+ AND revisions.workspace_id = outbox.workspace_id
2758
+ AND revisions.contract_version = outbox.contract_version
2759
+ AND revisions.domain = outbox.domain
2760
+ AND revisions.entity_id = outbox.entity_id
2761
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
2762
+ AND outbox.contract_version = ? AND outbox.domain = 'task'
2763
+ AND (outbox.mutation_kind = 'create'
2764
+ OR outbox.owns_task_metadata = 1
2765
+ OR outbox.owns_task_lifecycle = 1
2766
+ OR outbox.owns_task_assignee = 1
2767
+ OR outbox.mutation_kind IN ('metadata-update', 'delete', 'archive', 'unarchive', 'complete', 'reopen'))
2768
+ AND (
2769
+ outbox.status IN ('pending', 'in_flight', 'blocked')
2770
+ OR (outbox.mutation_kind = 'delete' AND outbox.status = 'accepted')
2771
+ )
2772
+ )
2773
+ SELECT entity_id,
2774
+ MAX(active_operation) AS active_operation,
2775
+ MAX(active_create_operation) AS active_create_operation,
2776
+ MAX(active_delete_operation) AS active_delete_operation,
2777
+ MAX(delete_owned) AS delete_owned,
2778
+ MAX(metadata_owned) AS metadata_owned,
2779
+ MAX(active_lifecycle_operation) AS active_lifecycle_operation,
2780
+ MAX(active_reopen_operation) AS active_reopen_operation,
2781
+ MAX(reopen_assignee_owned) AS reopen_assignee_owned,
2782
+ MAX(entity_revision) AS entity_revision,
2783
+ MAX(lifecycle_revision) AS lifecycle_revision,
2784
+ MAX(last_sequence) AS last_sequence
2785
+ FROM ownership_sources
2786
+ GROUP BY entity_id
2787
+ ORDER BY entity_id ASC
2788
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
2789
+ return rows.map((row) => ({
2790
+ taskId: requireText(row.entity_id, 'Task id'),
2791
+ activeOperation: Number(row.active_operation) === 1,
2792
+ activeCreateOperation: Number(row.active_create_operation) === 1,
2793
+ ...(Number(row.active_delete_operation) === 1 ? { activeDeleteOperation: true } : {}),
2794
+ ...(Number(row.delete_owned) === 1 ? { deleteOwned: true } : {}),
2795
+ metadataOwned: Number(row.metadata_owned) === 1,
2796
+ activeLifecycleOperation: Number(row.active_lifecycle_operation) === 1,
2797
+ activeReopenOperation: Number(row.active_reopen_operation) === 1,
2798
+ reopenAssigneeOwned: Number(row.reopen_assignee_owned) === 1,
2799
+ entityRevision: row.entity_revision === null || row.entity_revision === undefined
2800
+ ? null : normalizeSequence(row.entity_revision),
2801
+ lifecycleRevision: row.lifecycle_revision === null || row.lifecycle_revision === undefined
2802
+ ? null : normalizeSequence(row.lifecycle_revision),
2803
+ lastSequence: row.last_sequence === null || row.last_sequence === undefined
2804
+ ? null : normalizeSequence(row.last_sequence),
2805
+ }));
2806
+ }
2807
+ listTaskRevisionProjections(input) {
2808
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2809
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2810
+ const taskIds = Array.from(new Set(input.taskIds.map((taskId) => requireText(taskId, 'Task id')))).sort();
2811
+ if (taskIds.length === 0)
2812
+ return [];
2813
+ const placeholders = taskIds.map(() => '?').join(', ');
2814
+ const rows = this.db.prepare(`
2815
+ SELECT entity_id,
2816
+ CAST(entity_revision AS TEXT) AS entity_revision,
2817
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision,
2818
+ CAST(last_sequence AS TEXT) AS last_sequence,
2819
+ canonical_fingerprint,
2820
+ CASE WHEN EXISTS (
2821
+ SELECT 1 FROM workspace_sync_task_taxonomy_ownership AS taxonomy_ownership
2822
+ WHERE taxonomy_ownership.tenant_id = workspace_sync_entity_revisions.tenant_id
2823
+ AND taxonomy_ownership.workspace_id = workspace_sync_entity_revisions.workspace_id
2824
+ AND taxonomy_ownership.task_id = workspace_sync_entity_revisions.entity_id
2825
+ ) THEN 1 ELSE 0 END AS taxonomy_owned
2826
+ FROM workspace_sync_entity_revisions
2827
+ WHERE tenant_id = ? AND workspace_id = ?
2828
+ AND contract_version = ? AND domain = 'task'
2829
+ AND entity_id IN (${placeholders})
2830
+ AND entity_revision > 0 AND last_sequence IS NOT NULL
2831
+ AND canonical_fingerprint IS NOT NULL
2832
+ ORDER BY entity_id ASC
2833
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, ...taskIds);
2834
+ return rows.map((row) => ({
2835
+ taskId: requireText(row.entity_id, 'Task id'),
2836
+ entityRevision: normalizeSequence(row.entity_revision),
2837
+ lifecycleRevision: normalizeSequence(row.lifecycle_revision),
2838
+ lastSequence: normalizeSequence(row.last_sequence),
2839
+ canonicalFingerprint: requireText(row.canonical_fingerprint, 'Canonical fingerprint'),
2840
+ taxonomyOwned: Number(row.taxonomy_owned) === 1,
2841
+ }));
2842
+ }
2843
+ listPlanningRevisionProjections(input) {
2844
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2845
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2846
+ const domain = input.domain;
2847
+ const entityIds = Array.from(new Set(input.entityIds.map((entityId) => requireText(entityId, 'Planning entity id')))).sort();
2848
+ if (entityIds.length === 0)
2849
+ return [];
2850
+ if (entityIds.length > 100)
2851
+ throw new Error('Planning revision projection supports at most 100 entity ids.');
2852
+ const placeholders = entityIds.map(() => '?').join(', ');
2853
+ const rows = this.db.prepare(`
2854
+ SELECT revision.entity_id,
2855
+ revision.coexistence_owned,
2856
+ journal.mutation_kind,
2857
+ CAST(revision.entity_revision AS TEXT) AS entity_revision,
2858
+ CAST(revision.lifecycle_revision AS TEXT) AS lifecycle_revision,
2859
+ CAST(revision.last_sequence AS TEXT) AS last_sequence,
2860
+ revision.canonical_fingerprint,
2861
+ journal.occurred_at
2862
+ FROM workspace_sync_entity_revisions AS revision
2863
+ LEFT JOIN workspace_sync_journal AS journal
2864
+ ON journal.tenant_id = revision.tenant_id
2865
+ AND journal.workspace_id = revision.workspace_id
2866
+ AND journal.contract_version = revision.contract_version
2867
+ AND journal.domain = revision.domain
2868
+ AND journal.entity_id = revision.entity_id
2869
+ AND journal.sequence = revision.last_sequence
2870
+ WHERE revision.tenant_id = ? AND revision.workspace_id = ?
2871
+ AND revision.contract_version = ? AND revision.domain = ?
2872
+ AND revision.entity_id IN (${placeholders})
2873
+ AND revision.entity_revision > 0
2874
+ AND revision.last_sequence IS NOT NULL
2875
+ AND revision.canonical_fingerprint IS NOT NULL
2876
+ ORDER BY revision.entity_id ASC
2877
+ `).all(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, domain, ...entityIds);
2878
+ const allowedKinds = new Set(['create', 'metadata-update', 'append-comment', 'archive', 'unarchive']);
2879
+ return rows.map((row) => {
2880
+ const mutationKind = requireText(row.mutation_kind, 'Planning mutation kind');
2881
+ if (!allowedKinds.has(mutationKind)) {
2882
+ throw new Error(`Unsupported ${domain} journal mutation kind: ${mutationKind}`);
2883
+ }
2884
+ return {
2885
+ entityId: requireText(row.entity_id, 'Planning entity id'),
2886
+ coexistenceOwned: Number(row.coexistence_owned) === 1,
2887
+ mutationKind: mutationKind,
2888
+ entityRevision: normalizeSequence(row.entity_revision),
2889
+ lifecycleRevision: normalizeSequence(row.lifecycle_revision),
2890
+ lastSequence: normalizeSequence(row.last_sequence),
2891
+ canonicalFingerprint: requireText(row.canonical_fingerprint, 'Canonical fingerprint'),
2892
+ occurredAt: requireIsoTimestamp(row.occurred_at, 'Journal occurrence time'),
2893
+ };
2894
+ });
2895
+ }
2896
+ ensureInitiativeRevisionBaselines(input) {
2897
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2898
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2899
+ const now = requireIsoTimestamp(input.now, 'Initiative baseline time');
2900
+ const initiatives = [...input.initiatives].map((initiative) => ({
2901
+ ...initiative,
2902
+ entityId: requireText(initiative.entityId, 'Initiative id'),
2903
+ currentFingerprint: requireText(initiative.currentFingerprint, 'Initiative fingerprint'),
2904
+ occurredAt: requireIsoTimestamp(initiative.occurredAt, 'Initiative occurrence time'),
2905
+ })).sort((left, right) => compareCodeUnits(left.entityId, right.entityId));
2906
+ if (new Set(initiatives.map((initiative) => initiative.entityId)).size !== initiatives.length) {
2907
+ throw new Error('Initiative baseline contains duplicate entity ids.');
2908
+ }
2909
+ for (const initiative of initiatives) {
2910
+ if (!/^[a-f0-9]{64}$/.test(initiative.currentFingerprint)) {
2911
+ throw new Error(`Initiative baseline fingerprint is invalid: ${initiative.entityId}.`);
2912
+ }
2913
+ }
2914
+ return this.db.transaction(() => {
2915
+ this.allocateSequenceRange(tenantId, workspaceId, now, 0);
2916
+ const revisionRows = this.db.prepare(`
2917
+ SELECT entity_id, contract_version, canonical_fingerprint,
2918
+ CAST(entity_revision AS TEXT) AS entity_revision,
2919
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision,
2920
+ CAST(last_sequence AS TEXT) AS last_sequence
2921
+ FROM workspace_sync_entity_revisions
2922
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = 'initiative'
2923
+ `).all(tenantId, workspaceId);
2924
+ const revisions = new Map(revisionRows.map((row) => [requireText(row.entity_id, 'Initiative id'), row]));
2925
+ const missing = [];
2926
+ for (const initiative of initiatives) {
2927
+ const current = revisions.get(initiative.entityId);
2928
+ if (!current) {
2929
+ missing.push(initiative);
2930
+ continue;
2931
+ }
2932
+ if (Number(current.contract_version) !== WORKSPACE_SYNC_CONTRACT_VERSION
2933
+ || current.canonical_fingerprint !== initiative.currentFingerprint
2934
+ || BigInt(normalizeSequence(current.entity_revision)) < 1n
2935
+ || (initiative.archived && BigInt(normalizeSequence(current.lifecycle_revision)) < 1n)
2936
+ || BigInt(normalizeSequence(current.last_sequence)) < 1n) {
2937
+ throw new WorkspaceSyncRevisionConflictError();
2938
+ }
2939
+ }
2940
+ const rowCount = missing.reduce((count, initiative) => count + (initiative.archived ? 2 : 1), 0);
2941
+ const highWatermark = this.allocateSequenceRange(tenantId, workspaceId, now, rowCount);
2942
+ let sequence = BigInt(highWatermark) - BigInt(rowCount) + 1n;
2943
+ for (const initiative of missing) {
2944
+ const operationId = `initiative-baseline-${randomUUID()}`;
2945
+ const insertJournal = (mutationKind, entityRevision, lifecycleRevision, payload) => {
2946
+ this.db.prepare(`
2947
+ INSERT INTO workspace_sync_journal (
2948
+ tenant_id, workspace_id, sequence, contract_version, operation_id,
2949
+ client_id, domain, entity_id, mutation_kind, entity_revision,
2950
+ lifecycle_revision, content_sha256, size_bytes, payload_json, occurred_at
2951
+ ) VALUES (?, ?, ?, ?, ?, 'sync-v3-repair', 'initiative', ?, ?, ?, ?, NULL, NULL, ?, ?)
2952
+ `).run(tenantId, workspaceId, sequence.toString(10), WORKSPACE_SYNC_CONTRACT_VERSION, operationId, initiative.entityId, mutationKind, entityRevision, lifecycleRevision, canonicalWorkspaceSyncJson(payload), initiative.occurredAt);
2953
+ sequence += 1n;
2954
+ };
2955
+ insertJournal('create', 1, 0, initiative.activePayload);
2956
+ if (initiative.archived)
2957
+ insertJournal('archive', 2, 1, initiative.currentPayload);
2958
+ const entityRevision = initiative.archived ? 2 : 1;
2959
+ const lifecycleRevision = initiative.archived ? 1 : 0;
2960
+ const lastSequence = sequence - 1n;
2961
+ this.db.prepare(`
2962
+ INSERT INTO workspace_sync_entity_revisions (
2963
+ tenant_id, workspace_id, domain, entity_id, contract_version,
2964
+ entity_revision, lifecycle_revision, coexistence_owned, metadata_owned, reopen_assignee_owned,
2965
+ last_sequence, canonical_fingerprint, updated_at
2966
+ ) VALUES (?, ?, 'initiative', ?, ?, ?, ?, 0, 0, 0, ?, ?, ?)
2967
+ `).run(tenantId, workspaceId, initiative.entityId, WORKSPACE_SYNC_CONTRACT_VERSION, entityRevision, lifecycleRevision, lastSequence.toString(10), initiative.currentFingerprint, now);
2968
+ }
2969
+ return { highWatermark, insertedEntities: missing.length, insertedJournalRows: rowCount };
2970
+ })();
2971
+ }
2972
+ markTaskTaxonomyProjectionOwned(input) {
2973
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2974
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2975
+ const now = requireIsoTimestamp(input.now, 'Taxonomy ownership time');
2976
+ const taskIds = Array.from(new Set(input.taskIds.map((taskId) => requireText(taskId, 'Task id')))).sort();
2977
+ const insert = this.db.prepare(`
2978
+ INSERT INTO workspace_sync_task_taxonomy_ownership (
2979
+ tenant_id, workspace_id, task_id, created_at
2980
+ ) VALUES (?, ?, ?, ?)
2981
+ ON CONFLICT (tenant_id, workspace_id, task_id) DO NOTHING
2982
+ `);
2983
+ for (const taskId of taskIds)
2984
+ insert.run(tenantId, workspaceId, taskId, now);
2985
+ }
2986
+ listTaskTaxonomyProjectionOwnership(input) {
2987
+ const tenantId = requireText(input.tenantId, 'Tenant id');
2988
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
2989
+ const taskIds = Array.from(new Set(input.taskIds.map((taskId) => requireText(taskId, 'Task id')))).sort();
2990
+ if (taskIds.length === 0)
2991
+ return [];
2992
+ const placeholders = taskIds.map(() => '?').join(', ');
2993
+ const rows = this.db.prepare(`
2994
+ WITH ownership AS (
2995
+ SELECT task_id, 0 AS active_operation, 1 AS owned
2996
+ FROM workspace_sync_task_taxonomy_ownership
2997
+ WHERE tenant_id = ? AND workspace_id = ?
2998
+ AND task_id IN (${placeholders})
2999
+ UNION ALL
3000
+ SELECT target.entity_id AS task_id, 1 AS active_operation, 0 AS owned
3001
+ FROM workspace_sync_outbox_targets AS target
3002
+ INNER JOIN workspace_sync_outbox AS outbox
3003
+ ON outbox.tenant_id = target.tenant_id
3004
+ AND outbox.workspace_id = target.workspace_id
3005
+ AND outbox.client_id = target.client_id
3006
+ AND outbox.operation_id = target.operation_id
3007
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
3008
+ AND outbox.contract_version = ?
3009
+ AND outbox.status IN ('pending', 'in_flight', 'blocked')
3010
+ AND target.domain = 'task' AND target.mutation_kind = 'taxonomy-remap'
3011
+ AND target.entity_id IN (${placeholders})
3012
+ )
3013
+ SELECT task_id, MAX(active_operation) AS active_operation, MAX(owned) AS owned
3014
+ FROM ownership
3015
+ GROUP BY task_id
3016
+ ORDER BY task_id
3017
+ `).all(tenantId, workspaceId, ...taskIds, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, ...taskIds);
3018
+ return rows.map((row) => ({
3019
+ taskId: requireText(row.task_id, 'Task id'),
3020
+ activeOperation: Number(row.active_operation) === 1,
3021
+ owned: Number(row.owned) === 1,
3022
+ }));
3023
+ }
3024
+ listOutboxTargets(identity) {
3025
+ const ids = operationParams(identity);
3026
+ const rows = this.db.prepare(`
3027
+ SELECT domain, entity_id, mutation_kind,
3028
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
3029
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
3030
+ owns_task_metadata, owns_task_lifecycle, owns_task_assignee
3031
+ FROM workspace_sync_outbox_targets
3032
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3033
+ ORDER BY domain, entity_id
3034
+ `).all(...ids);
3035
+ return rows.map((row) => ({
3036
+ domain: requireText(row.domain, 'Outbox target domain'),
3037
+ entityId: requireText(row.entity_id, 'Outbox target entity id'),
3038
+ mutationKind: requireText(row.mutation_kind, 'Outbox target mutation kind'),
3039
+ baseEntityRevision: normalizeSequence(row.base_entity_revision),
3040
+ baseLifecycleRevision: normalizeSequence(row.base_lifecycle_revision),
3041
+ ownsTaskMetadata: Number(row.owns_task_metadata) === 1,
3042
+ ownsTaskLifecycle: Number(row.owns_task_lifecycle) === 1,
3043
+ ownsTaskAssignee: Number(row.owns_task_assignee) === 1,
3044
+ }));
3045
+ }
3046
+ runTaskMetadataCoexistenceTransaction(input) {
3047
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3048
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3049
+ const taskIds = Array.from(new Set(input.taskIds.map((taskId) => requireText(taskId, 'Task id')))).sort();
3050
+ return this.runWorkspaceDomainCoexistenceTransaction({
3051
+ tenantId,
3052
+ workspaceId,
3053
+ direction: input.direction,
3054
+ targets: taskIds.map((entityId) => ({ domain: 'task', entityId })),
3055
+ now: input.now,
3056
+ apply: () => input.apply(this.listTaskMetadataOwnership({ tenantId, workspaceId })),
3057
+ });
3058
+ }
3059
+ runWorkspaceDomainCoexistenceTransaction(input) {
3060
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3061
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3062
+ const now = requireIsoTimestamp(input.now, 'Coexistence lock timestamp');
3063
+ const targets = normalizeWorkspaceSyncLockTargets(input.targets);
3064
+ const localTargets = normalizeWorkspaceSyncLockTargets([
3065
+ ...targets,
3066
+ ...(targets.some((target) => target.domain === 'initiative') ? [
3067
+ { domain: 'initiative-assets', entityId: '__workspace__' },
3068
+ { domain: 'initiative-owners', entityId: '__workspace__' },
3069
+ ] : []),
3070
+ ...(targets.some((target) => target.domain === 'workstream') ? [
3071
+ { domain: 'workstream-assets', entityId: '__workspace__' },
3072
+ { domain: 'workstream-owners', entityId: '__workspace__' },
3073
+ ] : []),
3074
+ ]);
3075
+ return this.db.transaction(() => {
3076
+ if (input.direction === 'cloud-push') {
3077
+ // Cloud acceptance uses this row as the workspace-wide journal
3078
+ // serialization lock. A zero-width allocation acquires the same
3079
+ // lock without consuming a sequence.
3080
+ this.allocateSequenceRange(tenantId, workspaceId, now, 0);
3081
+ }
3082
+ else {
3083
+ // Local capture locks the same per-entity rows. Stable ordering
3084
+ // avoids deadlocks when one legacy page contains several domains.
3085
+ const lock = this.db.prepare(`
3086
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
3087
+ VALUES (?, ?, ?, ?)
3088
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
3089
+ `);
3090
+ if (requiresInitiativeProjectionLock(targets)) {
3091
+ lock.run(tenantId, workspaceId, WORKSPACE_SYNC_INITIATIVE_PROJECTION_LOCK, now);
3092
+ }
3093
+ if (requiresCombinedProjectionLock(targets)) {
3094
+ lock.run(tenantId, workspaceId, WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
3095
+ }
3096
+ for (const target of localTargets) {
3097
+ lock.run(tenantId, workspaceId, `local-outbox:${target.domain}:${target.entityId}`, now);
3098
+ }
3099
+ }
3100
+ return input.apply();
3101
+ })();
3102
+ }
3103
+ requeueLegacyPlanningAttachmentAcknowledgements(input) {
3104
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3105
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3106
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3107
+ const result = this.db.prepare(`
3108
+ UPDATE workspace_sync_outbox
3109
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3110
+ last_error_code = 'WORKSPACE_SYNC_PLANNING_ATTACHMENT_ACK_RECOVERY',
3111
+ last_error_message = 'Requeued after the compact planning attachment activity contract was repaired.',
3112
+ updated_at = ?
3113
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3114
+ AND mutation_kind = 'metadata-update'
3115
+ AND last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3116
+ AND (
3117
+ (domain = 'initiative' AND last_error_message = 'Initiative attachment link semantics are invalid.')
3118
+ OR (domain = 'workstream' AND last_error_message = 'Workstream attachment link semantics are invalid.')
3119
+ )
3120
+ `).run(now, now, tenantId, workspaceId);
3121
+ return Number(result.changes || 0);
3122
+ }
3123
+ requeueSupersededTaskAttachmentReferenceMetadataConflicts(input) {
3124
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3125
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3126
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3127
+ const result = this.db.prepare(`
3128
+ UPDATE workspace_sync_outbox
3129
+ SET status = 'pending', attempt_count = 0, rebase_before_first_attempt = 1,
3130
+ next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3131
+ task_attachment_reference_metadata_recovery_count =
3132
+ task_attachment_reference_metadata_recovery_count + 1,
3133
+ last_error_code = 'WORKSPACE_SYNC_TASK_ATTACHMENT_REFERENCE_METADATA_RECOVERY',
3134
+ last_error_message = 'Requeued against the current attachment-link revision after cloud writers began canonicalizing reference metadata by stable asset identity.',
3135
+ updated_at = ?
3136
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3137
+ AND domain = 'task-attachment-links' AND mutation_kind = 'replace'
3138
+ AND (
3139
+ (
3140
+ task_attachment_reference_metadata_recovery_count = 0
3141
+ AND last_error_code = 'TASK_ATTACHMENT_LINKS_INVALID'
3142
+ AND last_error_message LIKE 'Task attachment reference metadata does not match asset %.%'
3143
+ )
3144
+ OR (
3145
+ task_attachment_reference_metadata_recovery_count = 1
3146
+ AND rebase_before_first_attempt = 0
3147
+ AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3148
+ )
3149
+ )
3150
+ `).run(now, now, tenantId, workspaceId);
3151
+ return Number(result.changes || 0);
3152
+ }
3153
+ requeueSupersededOriginDispatchAuthorizationFailures(input) {
3154
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3155
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3156
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3157
+ const result = this.db.prepare(`
3158
+ UPDATE workspace_sync_outbox
3159
+ SET status = 'pending', next_attempt_at = ?,
3160
+ lease_owner = NULL, lease_expires_at = NULL,
3161
+ origin_assertion_recovery_count = origin_assertion_recovery_count + 1,
3162
+ last_error_code = 'WORKSPACE_SYNC_ORIGIN_ASSERTION_UPGRADE_RECOVERY',
3163
+ last_error_message = 'Requeued after signed origin assertions replaced the legacy local-dispatch trust header.',
3164
+ updated_at = ?
3165
+ WHERE tenant_id = ? AND workspace_id = ?
3166
+ AND status = 'blocked'
3167
+ AND origin_assertion_recovery_count = 0
3168
+ AND last_error_code = 'WORKSPACE_SYNC_V3_REQUEST_INVALID'
3169
+ AND last_error_message = 'originActor is only accepted from trusted local durable dispatch.'
3170
+ `).run(now, now, tenantId, workspaceId);
3171
+ return Number(result.changes || 0);
3172
+ }
3173
+ requeueSupersededTaskCreateChecklistAcknowledgements(input) {
3174
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3175
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3176
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3177
+ const result = this.db.prepare(`
3178
+ UPDATE workspace_sync_outbox
3179
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3180
+ last_error_code = 'WORKSPACE_SYNC_TASK_CREATE_CHECKLIST_ACK_RECOVERY',
3181
+ last_error_message = 'Requeued after task create acknowledgement began checking the captured checklist.',
3182
+ updated_at = ?
3183
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3184
+ AND domain = 'task' AND mutation_kind = 'create'
3185
+ AND last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3186
+ AND last_error_message = 'Authoritative task create response omitted the captured checklist projection.'
3187
+ `).run(now, now, tenantId, workspaceId);
3188
+ return Number(result.changes || 0);
3189
+ }
3190
+ requeueInitialTaskCreateRouteMisses(input) {
3191
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3192
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3193
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3194
+ const result = this.db.prepare(`
3195
+ UPDATE workspace_sync_outbox
3196
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3197
+ last_error_code = 'WORKSPACE_SYNC_TASK_CREATE_ROUTE_RECOVERY',
3198
+ last_error_message = 'Requeued once after the initial task create route was unavailable.',
3199
+ updated_at = ?
3200
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3201
+ AND domain = 'task' AND mutation_kind = 'create'
3202
+ AND attempt_count = 1
3203
+ AND last_error_code = 'WORKSPACE_SYNC_TRANSPORT_404'
3204
+ AND last_error_message = 'Not Found'
3205
+ `).run(now, now, tenantId, workspaceId);
3206
+ return Number(result.changes || 0);
3207
+ }
3208
+ requeueSupersededTaskCommentReconciliationConflicts(input) {
3209
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3210
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3211
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3212
+ const result = this.db.prepare(`
3213
+ UPDATE workspace_sync_outbox
3214
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3215
+ last_error_code = 'WORKSPACE_SYNC_TASK_COMMENT_RECONCILIATION_RECOVERY',
3216
+ last_error_message = 'Requeued once after task-comment reconciliation became revision-independent.',
3217
+ updated_at = ?
3218
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3219
+ AND domain = 'task-comments' AND mutation_kind = 'reconcile'
3220
+ AND attempt_count = 1
3221
+ AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3222
+ `).run(now, now, tenantId, workspaceId);
3223
+ return Number(result.changes || 0);
3224
+ }
3225
+ requeueTaskCommentRootAcknowledgementConflicts(input) {
3226
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3227
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3228
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3229
+ const result = this.db.prepare(`
3230
+ UPDATE workspace_sync_outbox
3231
+ SET status = 'pending', next_attempt_at = ?,
3232
+ lease_owner = NULL, lease_expires_at = NULL,
3233
+ accepted_replay_recovery_count = accepted_replay_recovery_count + 1,
3234
+ last_error_code = 'WORKSPACE_SYNC_TASK_COMMENT_ROOT_ACK_RECOVERY',
3235
+ last_error_message = 'Requeued once after task-comment acknowledgement stopped reinstalling unrelated task-root metadata.',
3236
+ updated_at = ?
3237
+ WHERE tenant_id = ? AND workspace_id = ?
3238
+ AND contract_version = ?
3239
+ AND status = 'blocked'
3240
+ AND domain = 'task-comments' AND mutation_kind = 'append'
3241
+ AND last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3242
+ AND last_error_message = 'Authoritative task root differs outside the comment mutation.'
3243
+ AND accepted_replay_recovery_count = 1
3244
+ AND recovery_batch_id IS NULL
3245
+ `).run(now, now, tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION);
3246
+ return Number(result.changes || 0);
3247
+ }
3248
+ requeueBoundedTaskStatusRevisionConflicts(input) {
3249
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3250
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3251
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3252
+ const result = this.db.prepare(`
3253
+ UPDATE workspace_sync_outbox
3254
+ SET status = 'pending', attempt_count = 0, rebase_before_first_attempt = 1,
3255
+ next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3256
+ task_status_revision_rebase_count = task_status_revision_rebase_count + 1,
3257
+ last_error_code = 'WORKSPACE_SYNC_TASK_STATUS_REVISION_REBASE',
3258
+ last_error_message = 'Requeued once against the current task revision after an initial conflict.',
3259
+ updated_at = ?
3260
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3261
+ AND domain = 'task' AND mutation_kind = 'set-status'
3262
+ AND attempt_count = 1
3263
+ AND task_status_revision_rebase_count < 1
3264
+ AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3265
+ `).run(now, now, tenantId, workspaceId);
3266
+ return Number(result.changes || 0);
3267
+ }
3268
+ // Workstream/initiative attachment-only metadata patches are Partial<>
3269
+ // merges, not full snapshots. The cloud accepts those patches against its
3270
+ // current revision, so this bounded retry can preserve unrelated concurrent
3271
+ // metadata. Other metadata patches remain exact-revision operations: this
3272
+ // selector may retry one once, but the cloud will reject it again rather
3273
+ // than allowing a stale title or structural edit to overwrite newer state.
3274
+ requeueBoundedWorkstreamMetadataRevisionConflicts(input) {
3275
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3276
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3277
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3278
+ const result = this.db.prepare(`
3279
+ UPDATE workspace_sync_outbox
3280
+ SET status = 'pending', attempt_count = 0, rebase_before_first_attempt = 1,
3281
+ next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3282
+ workstream_metadata_revision_rebase_count = workstream_metadata_revision_rebase_count + 1,
3283
+ last_error_code = 'WORKSPACE_SYNC_WORKSTREAM_METADATA_REVISION_REBASE',
3284
+ last_error_message = 'Requeued once against the current workstream revision after an initial conflict.',
3285
+ updated_at = ?
3286
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3287
+ AND domain = 'workstream' AND mutation_kind = 'metadata-update'
3288
+ AND attempt_count = 1
3289
+ AND workstream_metadata_revision_rebase_count < 1
3290
+ AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3291
+ `).run(now, now, tenantId, workspaceId);
3292
+ return Number(result.changes || 0);
3293
+ }
3294
+ requeueBoundedInitiativeMetadataRevisionConflicts(input) {
3295
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3296
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3297
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3298
+ const result = this.db.prepare(`
3299
+ UPDATE workspace_sync_outbox
3300
+ SET status = 'pending', attempt_count = 0, rebase_before_first_attempt = 1,
3301
+ next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3302
+ initiative_metadata_revision_rebase_count = initiative_metadata_revision_rebase_count + 1,
3303
+ last_error_code = 'WORKSPACE_SYNC_INITIATIVE_METADATA_REVISION_REBASE',
3304
+ last_error_message = 'Requeued once against the current initiative revision after an initial conflict.',
3305
+ updated_at = ?
3306
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3307
+ AND domain = 'initiative' AND mutation_kind = 'metadata-update'
3308
+ AND attempt_count = 1
3309
+ AND initiative_metadata_revision_rebase_count < 1
3310
+ AND last_error_code = 'WORKSPACE_SYNC_REVISION_CONFLICT'
3311
+ `).run(now, now, tenantId, workspaceId);
3312
+ return Number(result.changes || 0);
3313
+ }
3314
+ requeueTaskCommentProvenanceReconciliationConflicts(input) {
3315
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3316
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3317
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3318
+ const result = this.db.prepare(`
3319
+ UPDATE workspace_sync_outbox
3320
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3321
+ last_error_code = 'WORKSPACE_SYNC_TASK_COMMENT_PROVENANCE_RECOVERY',
3322
+ last_error_message = 'Requeued once after reconciliation began adopting authoritative comment provenance.',
3323
+ updated_at = ?
3324
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3325
+ AND domain = 'task-comments' AND mutation_kind = 'reconcile'
3326
+ AND attempt_count = 1
3327
+ AND (
3328
+ (
3329
+ last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3330
+ AND last_error_message LIKE 'Task comment identity conflict:%'
3331
+ )
3332
+ OR (
3333
+ last_error_code = 'TASK_COMMENT_IDENTITY_CONFLICT'
3334
+ AND last_error_message = 'Task comment identity conflict: reconciliation cannot change an authoritative comment.'
3335
+ )
3336
+ )
3337
+ `).run(now, now, tenantId, workspaceId);
3338
+ return Number(result.changes || 0);
3339
+ }
3340
+ supersedeArchivedReadOnlyTaskRecoveryIntents(input) {
3341
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3342
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3343
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3344
+ return this.db.transaction(() => {
3345
+ const rows = this.db.prepare(`
3346
+ SELECT client_id, operation_id, entity_id, mutation_kind,
3347
+ payload_json, recovery_batch_id
3348
+ FROM workspace_sync_outbox
3349
+ WHERE tenant_id = ? AND workspace_id = ?
3350
+ AND status = 'blocked'
3351
+ AND recovery_batch_id IS NOT NULL
3352
+ AND replay_of_operation_id IS NOT NULL
3353
+ AND domain = 'task'
3354
+ AND last_error_code = 'TASK_ARCHIVED_READ_ONLY'
3355
+ ORDER BY enqueue_sequence, created_at, operation_id
3356
+ ${this.db.provider === 'postgres' ? 'FOR UPDATE' : ''}
3357
+ `).all(tenantId, workspaceId);
3358
+ let superseded = 0;
3359
+ const affectedRecoveryBatchIds = new Set();
3360
+ const supersededEntities = new Set();
3361
+ for (const row of rows) {
3362
+ const taskId = requireText(row.entity_id, 'Task id');
3363
+ if (!input.isTaskArchived(taskId))
3364
+ continue;
3365
+ const recoveryBatchId = requireText(row.recovery_batch_id, 'Recovery batch id');
3366
+ const recoveryEntityKey = `${recoveryBatchId}\u0000${taskId}`;
3367
+ if (!supersededEntities.has(recoveryEntityKey)) {
3368
+ const lifecyclePrecedence = this.db.prepare(`
3369
+ SELECT current.lifecycle_revision,
3370
+ COUNT(*) AS unsettled_count,
3371
+ SUM(CASE WHEN replay.status = 'in_flight' THEN 1 ELSE 0 END)
3372
+ AS in_flight_count,
3373
+ SUM(CASE
3374
+ WHEN source.base_lifecycle_revision < current.lifecycle_revision
3375
+ THEN 0 ELSE 1
3376
+ END) AS not_predating_count
3377
+ FROM workspace_sync_outbox AS replay
3378
+ INNER JOIN workspace_sync_outbox AS source
3379
+ ON source.tenant_id = replay.tenant_id
3380
+ AND source.workspace_id = replay.workspace_id
3381
+ AND source.client_id = replay.replay_of_client_id
3382
+ AND source.operation_id = replay.replay_of_operation_id
3383
+ INNER JOIN workspace_sync_entity_revisions AS current
3384
+ ON current.tenant_id = replay.tenant_id
3385
+ AND current.workspace_id = replay.workspace_id
3386
+ AND current.domain = replay.domain
3387
+ AND current.entity_id = replay.entity_id
3388
+ WHERE replay.tenant_id = ? AND replay.workspace_id = ?
3389
+ AND replay.recovery_batch_id = ?
3390
+ AND replay.domain = 'task' AND replay.entity_id = ?
3391
+ AND replay.replay_of_operation_id IS NOT NULL
3392
+ AND replay.status IN ('pending', 'blocked', 'in_flight')
3393
+ GROUP BY current.lifecycle_revision
3394
+ `).get(tenantId, workspaceId, recoveryBatchId, taskId);
3395
+ if (Number(lifecyclePrecedence?.lifecycle_revision || 0) > 0
3396
+ && Number(lifecyclePrecedence?.unsettled_count || 0) > 0
3397
+ && Number(lifecyclePrecedence?.in_flight_count || 0) === 0
3398
+ && Number(lifecyclePrecedence?.not_predating_count || 0) === 0) {
3399
+ const updated = this.db.prepare(`
3400
+ UPDATE workspace_sync_outbox
3401
+ SET status = 'superseded',
3402
+ lease_owner = NULL, lease_expires_at = NULL,
3403
+ superseded_at = ?,
3404
+ last_error_code = 'WORKSPACE_SYNC_RECOVERY_INTENT_SUPERSEDED_BY_ARCHIVE',
3405
+ last_error_message = 'Recovered task intent was superseded because a newer authoritative archive lifecycle revision exists.',
3406
+ updated_at = ?
3407
+ WHERE tenant_id = ? AND workspace_id = ?
3408
+ AND recovery_batch_id = ?
3409
+ AND domain = 'task' AND entity_id = ?
3410
+ AND replay_of_operation_id IS NOT NULL
3411
+ AND status IN ('pending', 'blocked')
3412
+ `).run(now, now, tenantId, workspaceId, recoveryBatchId, taskId);
3413
+ const updatedCount = Number(updated.changes || 0);
3414
+ if (updatedCount > 0) {
3415
+ superseded += updatedCount;
3416
+ supersededEntities.add(recoveryEntityKey);
3417
+ affectedRecoveryBatchIds.add(recoveryBatchId);
3418
+ continue;
3419
+ }
3420
+ }
3421
+ }
3422
+ if (row.mutation_kind !== 'metadata-update')
3423
+ continue;
3424
+ let payload;
3425
+ try {
3426
+ payload = JSON.parse(String(row.payload_json || ''));
3427
+ }
3428
+ catch {
3429
+ continue;
3430
+ }
3431
+ const patch = payload.patch;
3432
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch))
3433
+ continue;
3434
+ const patchEntries = Object.entries(patch);
3435
+ const assignee = patchEntries[0]?.[1];
3436
+ if (patchEntries.length !== 1
3437
+ || patchEntries[0][0] !== 'assignee'
3438
+ || typeof assignee !== 'string'
3439
+ || !assignee.trim()) {
3440
+ continue;
3441
+ }
3442
+ const updated = this.db.prepare(`
3443
+ UPDATE workspace_sync_outbox
3444
+ SET status = 'superseded',
3445
+ lease_owner = NULL, lease_expires_at = NULL,
3446
+ superseded_at = ?,
3447
+ last_error_code = 'WORKSPACE_SYNC_RECOVERY_INTENT_SUPERSEDED_BY_ARCHIVE',
3448
+ last_error_message = 'Assignee-only recovery intent was superseded because the task is authoritatively archived.',
3449
+ updated_at = ?
3450
+ WHERE tenant_id = ? AND workspace_id = ?
3451
+ AND client_id = ? AND operation_id = ?
3452
+ AND status = 'blocked'
3453
+ AND last_error_code = 'TASK_ARCHIVED_READ_ONLY'
3454
+ `).run(now, now, tenantId, workspaceId, requireText(row.client_id, 'Client id'), requireText(row.operation_id, 'Operation id'));
3455
+ if (Number(updated.changes || 0) === 1) {
3456
+ superseded += 1;
3457
+ affectedRecoveryBatchIds.add(recoveryBatchId);
3458
+ }
3459
+ }
3460
+ for (const recoveryBatchId of affectedRecoveryBatchIds) {
3461
+ const unsettled = this.db.prepare(`
3462
+ SELECT COUNT(*) AS count
3463
+ FROM workspace_sync_outbox
3464
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
3465
+ AND replay_of_operation_id IS NOT NULL
3466
+ AND status NOT IN ('accepted', 'superseded')
3467
+ `).get(tenantId, workspaceId, recoveryBatchId);
3468
+ if (Number(unsettled.count || 0) !== 0)
3469
+ continue;
3470
+ this.db.prepare(`
3471
+ UPDATE workspace_sync_outbox_recoveries
3472
+ SET state = 'completed', completed_at = ?, updated_at = ?
3473
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
3474
+ AND state = 'replay_queued'
3475
+ AND EXISTS (
3476
+ SELECT 1
3477
+ FROM workspace_sync_outbox
3478
+ WHERE tenant_id = ? AND workspace_id = ? AND recovery_batch_id = ?
3479
+ AND replay_of_operation_id IS NOT NULL
3480
+ AND status = 'superseded'
3481
+ AND last_error_code = 'WORKSPACE_SYNC_RECOVERY_INTENT_SUPERSEDED_BY_ARCHIVE'
3482
+ )
3483
+ `).run(now, now, tenantId, workspaceId, recoveryBatchId, tenantId, workspaceId, recoveryBatchId);
3484
+ }
3485
+ return superseded;
3486
+ })();
3487
+ }
3488
+ requeueSupersededTaskStatusWorkflowAcknowledgements(input) {
3489
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3490
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3491
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3492
+ return this.db.transaction(() => {
3493
+ const candidates = this.db.prepare(`
3494
+ SELECT client_id, operation_id, payload_json
3495
+ FROM workspace_sync_outbox
3496
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3497
+ AND domain = 'task' AND mutation_kind = 'set-status'
3498
+ AND task_status_workflow_ack_recovery_count < 2
3499
+ AND last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3500
+ AND last_error_message = 'Authoritative workflow assignment does not match the persisted request.'
3501
+ `).all(tenantId, workspaceId);
3502
+ let recovered = 0;
3503
+ for (const candidate of candidates) {
3504
+ let payload;
3505
+ try {
3506
+ payload = JSON.parse(String(candidate.payload_json || ''));
3507
+ }
3508
+ catch {
3509
+ continue;
3510
+ }
3511
+ if (!['task', 'review', 'done', 'cancelled'].includes(String(payload.status || '')))
3512
+ continue;
3513
+ const result = this.db.prepare(`
3514
+ UPDATE workspace_sync_outbox
3515
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3516
+ task_status_workflow_ack_recovery_count = task_status_workflow_ack_recovery_count + 1,
3517
+ last_error_code = 'WORKSPACE_SYNC_TASK_STATUS_WORKFLOW_ACK_RECOVERY',
3518
+ last_error_message = 'Requeued after existing workflow assignment acknowledgement was repaired.',
3519
+ updated_at = ?
3520
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3521
+ AND status = 'blocked'
3522
+ AND task_status_workflow_ack_recovery_count < 2
3523
+ `).run(now, now, tenantId, workspaceId, requireText(candidate.client_id, 'Client id'), requireText(candidate.operation_id, 'Operation id'));
3524
+ recovered += Number(result.changes || 0);
3525
+ }
3526
+ return recovered;
3527
+ })();
3528
+ }
3529
+ requeueTaskStatusAssigneeAcknowledgements(input) {
3530
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3531
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3532
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3533
+ return this.db.transaction(() => {
3534
+ const candidates = this.db.prepare(`
3535
+ SELECT client_id, operation_id, payload_json
3536
+ FROM workspace_sync_outbox
3537
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3538
+ AND domain = 'task' AND mutation_kind = 'set-status'
3539
+ AND last_error_code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID'
3540
+ AND last_error_message = 'Authoritative task assignee does not match the persisted request.'
3541
+ `).all(tenantId, workspaceId);
3542
+ let recovered = 0;
3543
+ for (const candidate of candidates) {
3544
+ let payload;
3545
+ try {
3546
+ payload = JSON.parse(String(candidate.payload_json || ''));
3547
+ }
3548
+ catch {
3549
+ continue;
3550
+ }
3551
+ const assigneeIntent = payload.assigneeIntent;
3552
+ if (assigneeIntent?.kind !== 'set' || !String(assigneeIntent.value || '').trim())
3553
+ continue;
3554
+ const result = this.db.prepare(`
3555
+ UPDATE workspace_sync_outbox
3556
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3557
+ last_error_code = 'WORKSPACE_SYNC_TASK_STATUS_ASSIGNEE_ACK_RECOVERY',
3558
+ last_error_message = 'Requeued after task status acknowledgement began validating the durable assignee intent.',
3559
+ updated_at = ?
3560
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3561
+ AND status = 'blocked'
3562
+ `).run(now, now, tenantId, workspaceId, requireText(candidate.client_id, 'Client id'), requireText(candidate.operation_id, 'Operation id'));
3563
+ recovered += Number(result.changes || 0);
3564
+ }
3565
+ return recovered;
3566
+ })();
3567
+ }
3568
+ requeueTaskStatusWorkflowHistoryAcknowledgements(input) {
3569
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3570
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3571
+ const now = requireIsoTimestamp(input.now, 'Recovery time');
3572
+ return this.db.transaction(() => {
3573
+ const candidates = this.db.prepare(`
3574
+ SELECT client_id, operation_id, payload_json
3575
+ FROM workspace_sync_outbox
3576
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'blocked'
3577
+ AND domain = 'task' AND mutation_kind = 'set-status'
3578
+ AND recovery_batch_id IS NOT NULL
3579
+ AND replay_of_operation_id IS NOT NULL
3580
+ AND attempt_count = 1
3581
+ AND last_error_code = 'WORKFLOW_ASSIGNMENT_SYNC_CONFLICT'
3582
+ AND last_error_message = 'Workflow assignment history conflicts with canonical storage.'
3583
+ `).all(tenantId, workspaceId);
3584
+ let recovered = 0;
3585
+ for (const candidate of candidates) {
3586
+ let payload;
3587
+ try {
3588
+ payload = JSON.parse(String(candidate.payload_json || ''));
3589
+ }
3590
+ catch {
3591
+ continue;
3592
+ }
3593
+ if (payload.workflowAssignment != null)
3594
+ continue;
3595
+ const result = this.db.prepare(`
3596
+ UPDATE workspace_sync_outbox
3597
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3598
+ last_error_code = 'WORKSPACE_SYNC_TASK_STATUS_WORKFLOW_HISTORY_ACK_RECOVERY',
3599
+ last_error_message = 'Requeued after exact-prefix cloud workflow history preservation was enabled.',
3600
+ updated_at = ?
3601
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3602
+ AND status = 'blocked'
3603
+ AND recovery_batch_id IS NOT NULL
3604
+ AND replay_of_operation_id IS NOT NULL
3605
+ AND attempt_count = 1
3606
+ AND last_error_code = 'WORKFLOW_ASSIGNMENT_SYNC_CONFLICT'
3607
+ AND last_error_message = 'Workflow assignment history conflicts with canonical storage.'
3608
+ `).run(now, now, tenantId, workspaceId, requireText(candidate.client_id, 'Client id'), requireText(candidate.operation_id, 'Operation id'));
3609
+ recovered += Number(result.changes || 0);
3610
+ }
3611
+ return recovered;
3612
+ })();
3613
+ }
3614
+ claimOutbox(input) {
3615
+ const tenantId = requireText(input.tenantId, 'Tenant id');
3616
+ const workspaceId = requireText(input.workspaceId, 'Workspace id');
3617
+ const leaseOwner = requireText(input.leaseOwner || randomUUID(), 'Lease owner');
3618
+ const now = requireIsoTimestamp(input.now, 'Claim time');
3619
+ const leaseExpiresAt = requireIsoTimestamp(input.leaseExpiresAt, 'Lease expiry');
3620
+ if (leaseExpiresAt <= now)
3621
+ throw new Error('Lease expiry must be after the claim time.');
3622
+ const limit = Math.max(1, Math.min(100, Math.floor(Number(input.limit) || 0)));
3623
+ const dispatchClasses = (input.dispatchClasses || []).map(requireOutboxDispatchClass);
3624
+ const dispatchClassFilter = dispatchClasses.length > 0
3625
+ ? `AND outbox.dispatch_class IN (${dispatchClasses.map(() => '?').join(', ')})`
3626
+ : '';
3627
+ const records = this.db.transaction(() => {
3628
+ const futureDeadline = new Date(Date.parse(now) + MAX_OUTBOX_FUTURE_DEADLINE_MS).toISOString();
3629
+ // A corrected fast local clock must not strand an operation for years.
3630
+ // Legitimate dispatcher and server retry deadlines are capped at 24h.
3631
+ this.db.prepare(`
3632
+ UPDATE workspace_sync_outbox
3633
+ SET next_attempt_at = ?, updated_at = ?,
3634
+ last_error_code = 'WORKSPACE_SYNC_CLOCK_RECOVERY',
3635
+ last_error_message = 'Recovered an implausible future retry deadline after a local clock correction.'
3636
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'pending'
3637
+ AND next_attempt_at > ?
3638
+ `).run(now, now, tenantId, workspaceId, futureDeadline);
3639
+ this.db.prepare(`
3640
+ UPDATE workspace_sync_outbox
3641
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL, lease_expires_at = NULL,
3642
+ updated_at = ?, last_error_code = 'WORKSPACE_SYNC_CLOCK_RECOVERY',
3643
+ last_error_message = 'Recovered an implausible future lease after a local clock correction.'
3644
+ WHERE tenant_id = ? AND workspace_id = ? AND status = 'in_flight'
3645
+ AND lease_expires_at > ?
3646
+ `).run(now, now, tenantId, workspaceId, futureDeadline);
3647
+ const candidates = this.db.prepare(`
3648
+ SELECT outbox.client_id, outbox.operation_id
3649
+ FROM workspace_sync_outbox AS outbox
3650
+ WHERE outbox.tenant_id = ? AND outbox.workspace_id = ?
3651
+ AND ((outbox.status = 'pending' AND outbox.next_attempt_at <= ?)
3652
+ OR (outbox.status = 'in_flight' AND outbox.lease_expires_at <= ?))
3653
+ ${dispatchClassFilter}
3654
+ AND NOT EXISTS (
3655
+ SELECT 1 FROM workspace_sync_outbox AS earlier
3656
+ WHERE earlier.tenant_id = outbox.tenant_id
3657
+ AND earlier.workspace_id = outbox.workspace_id
3658
+ AND earlier.status IN ('pending', 'in_flight', 'blocked')
3659
+ AND EXISTS (
3660
+ SELECT 1
3661
+ FROM workspace_sync_outbox_targets AS earlier_target
3662
+ INNER JOIN workspace_sync_outbox_targets AS candidate_target
3663
+ ON candidate_target.tenant_id = earlier_target.tenant_id
3664
+ AND candidate_target.workspace_id = earlier_target.workspace_id
3665
+ AND candidate_target.domain = earlier_target.domain
3666
+ AND candidate_target.entity_id = earlier_target.entity_id
3667
+ WHERE earlier_target.tenant_id = earlier.tenant_id
3668
+ AND earlier_target.workspace_id = earlier.workspace_id
3669
+ AND earlier_target.client_id = earlier.client_id
3670
+ AND earlier_target.operation_id = earlier.operation_id
3671
+ AND candidate_target.client_id = outbox.client_id
3672
+ AND candidate_target.operation_id = outbox.operation_id
3673
+ )
3674
+ AND (
3675
+ earlier.enqueue_sequence < outbox.enqueue_sequence
3676
+ OR (earlier.enqueue_sequence = outbox.enqueue_sequence
3677
+ AND (earlier.created_at < outbox.created_at
3678
+ OR (earlier.created_at = outbox.created_at
3679
+ AND earlier.operation_id < outbox.operation_id)))
3680
+ )
3681
+ )
3682
+ ORDER BY outbox.next_attempt_at ASC, outbox.enqueue_sequence ASC,
3683
+ outbox.created_at ASC, outbox.operation_id ASC
3684
+ LIMIT ?
3685
+ `).all(tenantId, workspaceId, now, now, ...dispatchClasses, limit);
3686
+ const claimed = [];
3687
+ for (const candidate of candidates) {
3688
+ const clientId = requireText(candidate.client_id, 'Client id');
3689
+ const operationId = requireText(candidate.operation_id, 'Operation id');
3690
+ const queued = this.db.prepare(`
3691
+ SELECT domain, entity_id, attempt_count, rebase_before_first_attempt
3692
+ FROM workspace_sync_outbox
3693
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3694
+ `).get(tenantId, workspaceId, clientId, operationId);
3695
+ if (!queued)
3696
+ continue;
3697
+ if (Number(queued.rebase_before_first_attempt) === 1 && Number(queued.attempt_count) === 0) {
3698
+ const queuedDomain = requireText(queued.domain, 'Queued domain');
3699
+ const queuedEntityId = requireText(queued.entity_id, 'Queued entity id');
3700
+ const targets = this.db.prepare(`
3701
+ SELECT domain, entity_id
3702
+ FROM workspace_sync_outbox_targets
3703
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3704
+ ORDER BY domain, entity_id
3705
+ `).all(tenantId, workspaceId, clientId, operationId);
3706
+ let rebasedEntityRevision = '0';
3707
+ let rebasedLifecycleRevision = '0';
3708
+ for (const target of targets) {
3709
+ const targetDomain = requireText(target.domain, 'Queued target domain');
3710
+ const targetEntityId = requireText(target.entity_id, 'Queued target entity id');
3711
+ const revision = this.db.prepare(`
3712
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
3713
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision
3714
+ FROM workspace_sync_entity_revisions
3715
+ WHERE tenant_id = ? AND workspace_id = ? AND contract_version = ?
3716
+ AND domain = ? AND entity_id = ?
3717
+ `).get(tenantId, workspaceId, WORKSPACE_SYNC_CONTRACT_VERSION, targetDomain, targetEntityId);
3718
+ const targetEntityRevision = normalizeSequence(revision?.entity_revision ?? 0);
3719
+ const targetLifecycleRevision = normalizeSequence(revision?.lifecycle_revision ?? 0);
3720
+ this.db.prepare(`
3721
+ UPDATE workspace_sync_outbox_targets
3722
+ SET base_entity_revision = ?, base_lifecycle_revision = ?
3723
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3724
+ AND domain = ? AND entity_id = ?
3725
+ `).run(targetEntityRevision, targetLifecycleRevision, tenantId, workspaceId, clientId, operationId, targetDomain, targetEntityId);
3726
+ if (targetDomain === queuedDomain && targetEntityId === queuedEntityId) {
3727
+ rebasedEntityRevision = targetEntityRevision;
3728
+ rebasedLifecycleRevision = targetLifecycleRevision;
3729
+ }
3730
+ }
3731
+ this.db.prepare(`
3732
+ UPDATE workspace_sync_outbox
3733
+ SET base_entity_revision = ?, base_lifecycle_revision = ?,
3734
+ rebase_before_first_attempt = 0, updated_at = ?
3735
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3736
+ AND status = 'pending' AND attempt_count = 0
3737
+ `).run(rebasedEntityRevision, rebasedLifecycleRevision, now, tenantId, workspaceId, clientId, operationId);
3738
+ }
3739
+ const updated = this.db.prepare(`
3740
+ UPDATE workspace_sync_outbox
3741
+ SET status = 'in_flight', attempt_count = attempt_count + 1,
3742
+ lease_owner = ?, lease_expires_at = ?, updated_at = ?
3743
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3744
+ AND ((status = 'pending' AND next_attempt_at <= ?)
3745
+ OR (status = 'in_flight' AND lease_expires_at <= ?))
3746
+ `).run(leaseOwner, leaseExpiresAt, now, tenantId, workspaceId, clientId, operationId, now, now);
3747
+ if (Number(updated.changes || 0) === 1)
3748
+ claimed.push({ clientId, operationId });
3749
+ }
3750
+ return claimed.map(({ clientId, operationId }) => {
3751
+ const row = this.db.prepare(`
3752
+ SELECT tenant_id, workspace_id, client_id, operation_id, dispatch_class, domain, entity_id,
3753
+ mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
3754
+ CAST(base_entity_revision AS TEXT) AS base_entity_revision,
3755
+ CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
3756
+ payload_json, payload_fingerprint, status, attempt_count,
3757
+ initial_not_found_attempt_count,
3758
+ CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
3759
+ rebase_before_first_attempt,
3760
+ workstream_metadata_revision_rebase_count,
3761
+ initiative_metadata_revision_rebase_count,
3762
+ lease_owner, lease_expires_at
3763
+ FROM workspace_sync_outbox
3764
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3765
+ AND status = 'in_flight' AND lease_owner = ?
3766
+ `).get(tenantId, workspaceId, clientId, operationId, leaseOwner);
3767
+ if (!row)
3768
+ throw new WorkspaceSyncLeaseLostError();
3769
+ return this.mapOutboxRow(row);
3770
+ });
3771
+ })();
3772
+ return { leaseOwner, records };
3773
+ }
3774
+ releaseOutboxClaim(input) {
3775
+ const ids = operationParams(input.identity);
3776
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
3777
+ const now = requireIsoTimestamp(input.now, 'Release time');
3778
+ const nextAttemptAt = requireIsoTimestamp(input.nextAttemptAt, 'Next attempt time');
3779
+ const result = this.db.prepare(`
3780
+ UPDATE workspace_sync_outbox
3781
+ SET status = 'pending', next_attempt_at = ?, lease_owner = NULL,
3782
+ lease_expires_at = NULL, last_error_code = ?, last_error_message = ?,
3783
+ initial_not_found_attempt_count = initial_not_found_attempt_count + ?, updated_at = ?
3784
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3785
+ AND status = 'in_flight' AND lease_owner = ? AND lease_expires_at > ?
3786
+ `).run(nextAttemptAt, input.errorCode ?? null, input.errorMessage ?? null, input.incrementInitialNotFoundAttemptCount === true ? 1 : 0, now, ...ids, leaseOwner, now);
3787
+ if (Number(result.changes || 0) !== 1)
3788
+ throw new WorkspaceSyncLeaseLostError();
3789
+ }
3790
+ blockOutboxClaim(input) {
3791
+ const ids = operationParams(input.identity);
3792
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
3793
+ const now = requireIsoTimestamp(input.now, 'Block time');
3794
+ const errorCode = requireText(input.errorCode, 'Error code');
3795
+ const errorMessage = requireText(input.errorMessage, 'Error message');
3796
+ const result = this.db.prepare(`
3797
+ UPDATE workspace_sync_outbox
3798
+ SET status = 'blocked', lease_owner = NULL, lease_expires_at = NULL,
3799
+ last_error_code = ?, last_error_message = ?, updated_at = ?
3800
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3801
+ AND status = 'in_flight' AND lease_owner = ? AND lease_expires_at > ?
3802
+ `).run(errorCode, errorMessage, now, ...ids, leaseOwner, now);
3803
+ if (Number(result.changes || 0) !== 1)
3804
+ throw new WorkspaceSyncLeaseLostError();
3805
+ }
3806
+ applyAuthoritativeAndAck(input) {
3807
+ const ids = operationParams(input.identity);
3808
+ const leaseOwner = requireText(input.leaseOwner, 'Lease owner');
3809
+ const now = requireIsoTimestamp(input.now, 'Acknowledgement time');
3810
+ const responseJson = canonicalWorkspaceSyncJson(input.response);
3811
+ const acceptedSequence = normalizeSequence(input.acceptedSequence);
3812
+ const rawAuthoritative = input.authoritative
3813
+ ? (Array.isArray(input.authoritative) ? input.authoritative : [input.authoritative])
3814
+ : [];
3815
+ const authoritative = rawAuthoritative.map((entry) => ({
3816
+ domain: requireText(entry.domain, 'Authoritative domain'),
3817
+ entityId: requireText(entry.entityId, 'Authoritative entity id'),
3818
+ mutationKind: requireText(entry.mutationKind, 'Authoritative mutation kind'),
3819
+ entityRevision: normalizeSequence(entry.entityRevision),
3820
+ lifecycleRevision: normalizeSequence(entry.lifecycleRevision),
3821
+ sequence: normalizeSequence(entry.sequence),
3822
+ canonicalFingerprint: requireText(entry.canonicalFingerprint, 'Authoritative canonical fingerprint'),
3823
+ metadataOwned: entry.metadataOwned === true,
3824
+ reopenAssigneeOwned: entry.reopenAssigneeOwned === true,
3825
+ allowAuthoritativeRevisionReset: entry.allowAuthoritativeRevisionReset === true,
3826
+ }));
3827
+ for (const entry of authoritative) {
3828
+ if (entry.allowAuthoritativeRevisionReset
3829
+ && (entry.domain !== 'task-comments' || entry.mutationKind !== 'reconcile')) {
3830
+ throw new Error('Authoritative revision reset is reserved for task-comment reconciliation.');
3831
+ }
3832
+ }
3833
+ const authoritativeKeys = new Set(authoritative.map((entry) => `${entry.domain}\u0000${entry.entityId}`));
3834
+ const authoritativeSequences = new Set(authoritative.map((entry) => entry.sequence));
3835
+ const expectedFirstSequence = BigInt(acceptedSequence) - BigInt(Math.max(0, authoritative.length - 1));
3836
+ const sortedSequences = authoritative
3837
+ .map((entry) => BigInt(entry.sequence))
3838
+ .sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
3839
+ const hasContiguousSequenceRange = sortedSequences.every((sequence, index) => sequence === expectedFirstSequence + BigInt(index));
3840
+ if (authoritativeKeys.size !== authoritative.length
3841
+ || authoritativeSequences.size !== authoritative.length
3842
+ || (authoritative.length > 0 && (!hasContiguousSequenceRange || expectedFirstSequence < 0n))) {
3843
+ throw new Error('Authoritative revisions must be distinct and cover the accepted contiguous sequence range.');
3844
+ }
3845
+ return this.db.transaction(() => {
3846
+ if (authoritative.length > 0) {
3847
+ const lock = this.db.prepare(`
3848
+ INSERT INTO workspace_sync_domain_locks (tenant_id, workspace_id, domain, updated_at)
3849
+ VALUES (?, ?, ?, ?)
3850
+ ON CONFLICT (tenant_id, workspace_id, domain) DO UPDATE SET updated_at = excluded.updated_at
3851
+ `);
3852
+ if (requiresInitiativeProjectionLock(authoritative)) {
3853
+ lock.run(ids[0], ids[1], WORKSPACE_SYNC_INITIATIVE_PROJECTION_LOCK, now);
3854
+ }
3855
+ if (requiresCombinedProjectionLock(authoritative)) {
3856
+ lock.run(ids[0], ids[1], WORKSPACE_SYNC_COMBINED_PROJECTION_LOCK, now);
3857
+ }
3858
+ for (const entry of normalizeWorkspaceSyncLockTargets(authoritative)) {
3859
+ lock.run(ids[0], ids[1], `local-outbox:${entry.domain}:${entry.entityId}`, now);
3860
+ }
3861
+ }
3862
+ const owned = this.db.prepare(`
3863
+ UPDATE workspace_sync_outbox SET updated_at = ?
3864
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3865
+ AND status = 'in_flight' AND lease_owner = ? AND lease_expires_at > ?
3866
+ `).run(now, ...ids, leaseOwner, now);
3867
+ if (Number(owned.changes || 0) !== 1)
3868
+ throw new WorkspaceSyncLeaseLostError();
3869
+ if (authoritative.length > 0) {
3870
+ const outbox = this.db.prepare(`
3871
+ SELECT domain, entity_id, mutation_kind FROM workspace_sync_outbox
3872
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3873
+ AND status = 'in_flight' AND lease_owner = ?
3874
+ `).get(...ids, leaseOwner);
3875
+ const root = authoritative.find((entry) => (entry.domain === outbox?.domain
3876
+ && entry.entityId === outbox?.entity_id
3877
+ && entry.mutationKind === outbox?.mutation_kind));
3878
+ if (!root) {
3879
+ throw new WorkspaceSyncIdempotencyConflictError();
3880
+ }
3881
+ const readCurrentRevision = this.db.prepare(`
3882
+ SELECT CAST(entity_revision AS TEXT) AS entity_revision,
3883
+ CAST(lifecycle_revision AS TEXT) AS lifecycle_revision,
3884
+ CAST(last_sequence AS TEXT) AS last_sequence
3885
+ FROM workspace_sync_entity_revisions
3886
+ WHERE tenant_id = ? AND workspace_id = ? AND domain = ? AND entity_id = ?
3887
+ `);
3888
+ for (const entry of authoritative) {
3889
+ const currentRevision = readCurrentRevision.get(ids[0], ids[1], entry.domain, entry.entityId);
3890
+ if ((!entry.allowAuthoritativeRevisionReset && (BigInt(entry.entityRevision) < BigInt(normalizeSequence(currentRevision?.entity_revision ?? 0))
3891
+ || BigInt(entry.lifecycleRevision)
3892
+ < BigInt(normalizeSequence(currentRevision?.lifecycle_revision ?? 0))))
3893
+ || BigInt(entry.sequence) < BigInt(normalizeSequence(currentRevision?.last_sequence ?? 0))) {
3894
+ throw new WorkspaceSyncRevisionConflictError();
3895
+ }
3896
+ }
3897
+ }
3898
+ const result = input.applyAuthoritative();
3899
+ if (authoritative.length > 0) {
3900
+ const upsertRevision = this.db.prepare(`
3901
+ INSERT INTO workspace_sync_entity_revisions (
3902
+ tenant_id, workspace_id, domain, entity_id, contract_version,
3903
+ entity_revision, lifecycle_revision, coexistence_owned, metadata_owned, reopen_assignee_owned, last_sequence,
3904
+ canonical_fingerprint, updated_at
3905
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
3906
+ ON CONFLICT (tenant_id, workspace_id, domain, entity_id) DO UPDATE SET
3907
+ contract_version = excluded.contract_version,
3908
+ entity_revision = excluded.entity_revision,
3909
+ lifecycle_revision = excluded.lifecycle_revision,
3910
+ coexistence_owned = 1,
3911
+ metadata_owned = CASE
3912
+ WHEN excluded.metadata_owned = 1 THEN 1
3913
+ ELSE workspace_sync_entity_revisions.metadata_owned
3914
+ END,
3915
+ reopen_assignee_owned = CASE
3916
+ WHEN excluded.reopen_assignee_owned = 1 THEN 1
3917
+ ELSE workspace_sync_entity_revisions.reopen_assignee_owned
3918
+ END,
3919
+ last_sequence = excluded.last_sequence,
3920
+ canonical_fingerprint = excluded.canonical_fingerprint,
3921
+ updated_at = excluded.updated_at
3922
+ `);
3923
+ for (const entry of authoritative) {
3924
+ upsertRevision.run(ids[0], ids[1], entry.domain, entry.entityId, WORKSPACE_SYNC_CONTRACT_VERSION, entry.entityRevision, entry.lifecycleRevision, entry.domain === 'task' && (entry.mutationKind === 'metadata-update'
3925
+ || entry.mutationKind === 'create'
3926
+ || entry.metadataOwned === true) ? 1 : 0, entry.domain === 'task' && entry.reopenAssigneeOwned === true ? 1 : 0, entry.sequence, entry.canonicalFingerprint, now);
3927
+ }
3928
+ }
3929
+ const acked = this.db.prepare(`
3930
+ UPDATE workspace_sync_outbox
3931
+ SET status = 'accepted', authoritative_response_json = ?, accepted_sequence = ?,
3932
+ lease_owner = NULL, lease_expires_at = NULL,
3933
+ last_error_code = NULL, last_error_message = NULL, updated_at = ?
3934
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
3935
+ AND status = 'in_flight' AND lease_owner = ? AND lease_expires_at > ?
3936
+ `).run(responseJson, acceptedSequence, now, ...ids, leaseOwner, now);
3937
+ if (Number(acked.changes || 0) !== 1)
3938
+ throw new WorkspaceSyncLeaseLostError();
3939
+ return result;
3940
+ })();
3941
+ }
3942
+ isTargetStateLifecycleConflict(source) {
3943
+ const mutationKind = String(source.mutation_kind || '');
3944
+ const errorCode = String(source.last_error_code || '');
3945
+ if (mutationKind === 'archive' && errorCode === 'TASK_LIFECYCLE_ALREADY_ARCHIVED')
3946
+ return true;
3947
+ if (mutationKind === 'unarchive' && errorCode === 'TASK_LIFECYCLE_NOT_ARCHIVED')
3948
+ return true;
3949
+ if (errorCode !== 'TASK_LIFECYCLE_STATE_CONFLICT')
3950
+ return false;
3951
+ const legacyMessage = String(source.last_error_message || '');
3952
+ // Persisted pre-0.3.327 rows used one overloaded code. Exact legacy
3953
+ // messages are recognized only to drain already-blocked recoveries;
3954
+ // new writes use the typed codes above.
3955
+ return (mutationKind === 'archive' && legacyMessage === 'Task is already archived.')
3956
+ || (mutationKind === 'unarchive' && legacyMessage === 'Task is not archived.');
3957
+ }
3958
+ isTaskLifecycleRecoveryIntentSatisfied(tenantId, workspaceId, source) {
3959
+ const mutationKind = String(source.mutation_kind || '');
3960
+ if (mutationKind !== 'archive' && mutationKind !== 'unarchive')
3961
+ return false;
3962
+ let payload;
3963
+ try {
3964
+ payload = JSON.parse(String(source.payload_json || ''));
3965
+ }
3966
+ catch {
3967
+ return false;
3968
+ }
3969
+ if (mutationKind === 'archive'
3970
+ && (payload.cancellation !== false || payload.reason !== null)) {
3971
+ return false;
3972
+ }
3973
+ const task = this.db.prepare(`
3974
+ SELECT status, is_archived, completed_at, canceled_reason
3975
+ FROM tasks
3976
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
3977
+ `).get(tenantId, workspaceId, requireText(source.entity_id, 'Recovery task id'));
3978
+ if (!task)
3979
+ return false;
3980
+ const isArchived = task.is_archived === true || Number(task.is_archived || 0) === 1;
3981
+ const status = String(task.status || '').trim().toLowerCase();
3982
+ const completedAt = typeof task.completed_at === 'string' && task.completed_at.trim()
3983
+ ? task.completed_at.trim()
3984
+ : null;
3985
+ const canceledReason = typeof task.canceled_reason === 'string' && task.canceled_reason.trim()
3986
+ ? task.canceled_reason.trim()
3987
+ : null;
3988
+ if (mutationKind === 'archive') {
3989
+ return isArchived && status === 'done' && completedAt !== null && canceledReason === null;
3990
+ }
3991
+ return !isArchived && status === 'task' && completedAt === null && canceledReason === null;
3992
+ }
3993
+ normalizeRecoveredWorkflowStepEvidence(tenantId, workspaceId, assignment) {
3994
+ const version = assignment.version;
3995
+ const requestedSteps = Array.isArray(assignment.steps) ? assignment.steps : null;
3996
+ const workflowVersionId = String(version?.id || '');
3997
+ if (!workflowVersionId || !requestedSteps)
3998
+ return null;
3999
+ const workflowStepsTable = this.db.provider === 'postgres'
4000
+ ? this.db.prepare(`
4001
+ SELECT 1 AS present
4002
+ FROM information_schema.tables
4003
+ WHERE table_schema = current_schema() AND table_name = 'workflow_steps'
4004
+ `).get()
4005
+ : this.db.prepare(`
4006
+ SELECT 1 AS present
4007
+ FROM sqlite_master
4008
+ WHERE type = 'table' AND name = 'workflow_steps'
4009
+ `).get();
4010
+ if (!workflowStepsTable)
4011
+ return null;
4012
+ const rows = this.db.prepare(`
4013
+ SELECT id, tenant_id, workspace_id, workflow_version_id, position,
4014
+ name, instructions, owner_type, owner_id, expected_artifact,
4015
+ requires_comment_evidence, is_required, created_at, updated_at
4016
+ FROM workflow_steps
4017
+ WHERE tenant_id = ? AND workspace_id = ? AND workflow_version_id = ?
4018
+ ORDER BY position, id
4019
+ `).all(tenantId, workspaceId, workflowVersionId);
4020
+ if (rows.length !== requestedSteps.length)
4021
+ return null;
4022
+ const canonicalById = new Map(rows.map((row) => {
4023
+ const step = {
4024
+ id: requireText(row.id, 'Workflow step id'),
4025
+ tenantId: requireText(row.tenant_id, 'Workflow step tenant id'),
4026
+ workspaceId: requireText(row.workspace_id, 'Workflow step workspace id'),
4027
+ workflowVersionId: requireText(row.workflow_version_id, 'Workflow step version id'),
4028
+ position: Number(row.position),
4029
+ name: requireText(row.name, 'Workflow step name'),
4030
+ instructions: String(row.instructions || ''),
4031
+ ownerType: row.owner_type === 'user' || row.owner_type === 'agent'
4032
+ ? row.owner_type
4033
+ : null,
4034
+ ownerId: row.owner_id == null ? null : String(row.owner_id),
4035
+ expectedArtifact: row.expected_artifact == null ? null : String(row.expected_artifact),
4036
+ requiresCommentEvidence: Number(row.requires_comment_evidence) === 1,
4037
+ isRequired: Number(row.is_required) !== 0,
4038
+ createdAt: requireText(row.created_at, 'Workflow step created time'),
4039
+ updatedAt: requireText(row.updated_at, 'Workflow step updated time'),
4040
+ };
4041
+ return [step.id, step];
4042
+ }));
4043
+ let changed = false;
4044
+ const normalizedSteps = requestedSteps.map((rawStep) => {
4045
+ if (!rawStep || typeof rawStep !== 'object' || Array.isArray(rawStep))
4046
+ return null;
4047
+ const requested = rawStep;
4048
+ const canonical = canonicalById.get(String(requested.id || ''));
4049
+ if (!canonical)
4050
+ return null;
4051
+ const { requiresCommentEvidence: _requestedEvidence, ...requestedDefinition } = requested;
4052
+ const { requiresCommentEvidence: canonicalEvidence, ...canonicalDefinition } = canonical;
4053
+ if (canonicalWorkspaceSyncJson(requestedDefinition)
4054
+ !== canonicalWorkspaceSyncJson(canonicalDefinition)) {
4055
+ return null;
4056
+ }
4057
+ if (requested.requiresCommentEvidence !== canonicalEvidence) {
4058
+ if (requested.requiresCommentEvidence !== true || canonicalEvidence !== false) {
4059
+ return null;
4060
+ }
4061
+ changed = true;
4062
+ }
4063
+ return {
4064
+ ...requested,
4065
+ requiresCommentEvidence: canonicalEvidence,
4066
+ };
4067
+ });
4068
+ if (normalizedSteps.some((step) => step === null))
4069
+ return null;
4070
+ return {
4071
+ assignment: {
4072
+ ...assignment,
4073
+ steps: normalizedSteps,
4074
+ },
4075
+ changed,
4076
+ };
4077
+ }
4078
+ prepareRecoveryPayload(tenantId, workspaceId, source) {
4079
+ if (source.domain === 'task-comments' && source.mutation_kind === 'append') {
4080
+ return this.prepareTaskCommentAppendRecoveryPayload(tenantId, workspaceId, source);
4081
+ }
4082
+ if (source.dispatch_class === 'legacy-adoption'
4083
+ && source.domain === 'entity-event'
4084
+ && source.mutation_kind === 'adopt-legacy-batch') {
4085
+ const payload = this.readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source);
4086
+ const payloadJson = canonicalWorkspaceSyncJson(payload);
4087
+ return {
4088
+ payloadJson,
4089
+ payloadFingerprint: fingerprintWorkspaceSyncPayload(payload),
4090
+ replayMutationKind: 'adopt-legacy-batch',
4091
+ workflowDependencyCoalesced: false,
4092
+ };
4093
+ }
4094
+ return {
4095
+ ...this.prepareTaskRecoveryPayload(tenantId, workspaceId, source),
4096
+ replayMutationKind: requireText(source.mutation_kind, 'Source mutation kind'),
4097
+ };
4098
+ }
4099
+ parseFrozenRecoveryPayload(source) {
4100
+ let payload;
4101
+ try {
4102
+ const parsed = JSON.parse(String(source.payload_json || ''));
4103
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
4104
+ throw new Error('invalid');
4105
+ }
4106
+ payload = parsed;
4107
+ }
4108
+ catch {
4109
+ throw new Error('Workspace sync recovery payload is not valid JSON.');
4110
+ }
4111
+ if (fingerprintWorkspaceSyncPayload(payload) !== source.payload_fingerprint) {
4112
+ throw new Error('Workspace sync recovery payload fingerprint does not match.');
4113
+ }
4114
+ return payload;
4115
+ }
4116
+ readEntityEventLegacyAdoptionRecoveryIntent(workspaceId, source) {
4117
+ const payload = this.parseFrozenRecoveryPayload(source);
4118
+ if (Object.keys(payload).length !== 1 || !Array.isArray(payload.events)
4119
+ || payload.events.length < 1 || payload.events.length > 100) {
4120
+ throw new Error('Legacy entity-event adoption recovery payload is invalid.');
4121
+ }
4122
+ if (Buffer.byteLength(canonicalWorkspaceSyncJson(payload), 'utf8')
4123
+ > LEGACY_ENTITY_EVENT_ADOPTION_MAX_PAYLOAD_BYTES) {
4124
+ throw new Error('Legacy entity-event adoption recovery payload exceeds the transport-safe byte limit.');
4125
+ }
4126
+ const events = payload.events.map((value) => validateWorkspaceSyncV3EntityEventPayload(value));
4127
+ if (events.some((event) => event.workspaceId !== workspaceId)) {
4128
+ throw new Error('Legacy entity-event adoption recovery event identity is invalid.');
4129
+ }
4130
+ if (events[0].id !== source.entity_id
4131
+ || new Set(events.map((event) => event.id)).size !== events.length) {
4132
+ throw new Error('Legacy entity-event adoption recovery batch identity is invalid.');
4133
+ }
4134
+ return { events };
4135
+ }
4136
+ readTaskCreateRecoveryIntent(source) {
4137
+ const payload = this.parseFrozenRecoveryPayload(source);
4138
+ const allowedPayloadKeys = new Set(['task', 'localCarriers', 'originActor']);
4139
+ if (Object.keys(payload).some((key) => !allowedPayloadKeys.has(key))) {
4140
+ throw new Error('Task create recovery payload contains unsupported fields.');
4141
+ }
4142
+ if (payload.originActor !== undefined)
4143
+ parseWorkspaceSyncV3OriginActor(payload.originActor);
4144
+ if (!payload.task || typeof payload.task !== 'object' || Array.isArray(payload.task)) {
4145
+ throw new Error('Task create recovery payload is invalid.');
4146
+ }
4147
+ const task = payload.task;
4148
+ if (Object.keys(task).some((key) => !TASK_CREATE_FIELDS.has(key))) {
4149
+ throw new Error('Task create recovery payload contains unsupported task fields.');
4150
+ }
4151
+ if (typeof task.title !== 'string' || !task.title.trim() || task.title !== task.title.trim()) {
4152
+ throw new Error('Task create recovery payload title is not canonical.');
4153
+ }
4154
+ const carriers = payload.localCarriers;
4155
+ if (!carriers || typeof carriers !== 'object' || Array.isArray(carriers)
4156
+ || canonicalWorkspaceSyncJson(Object.keys(carriers).sort())
4157
+ !== canonicalWorkspaceSyncJson(['attachments', 'checklistItems', 'comments'])
4158
+ || !Array.isArray(carriers.attachments)
4159
+ || !Array.isArray(carriers.checklistItems)
4160
+ || !Array.isArray(carriers.comments)
4161
+ || carriers.attachments.length > 0
4162
+ || carriers.checklistItems.length > 0
4163
+ || carriers.comments.length > 0) {
4164
+ throw new Error('Task create recovery requires empty local carriers; nested content needs explicit recovery.');
4165
+ }
4166
+ return { task };
4167
+ }
4168
+ assertTaskCreateRecoveryIntentSatisfied(tenantId, workspaceId, source) {
4169
+ if (!TASK_CREATE_RECOVERY_ERROR_CODES.has(String(source.last_error_code || ''))) {
4170
+ throw new Error('Task create recovery requires an authoritative identity or revision conflict.');
4171
+ }
4172
+ const { task } = this.readTaskCreateRecoveryIntent(source);
4173
+ const installed = this.db.prepare(`
4174
+ SELECT title, description, priority, complexity, type, category, assignee, status,
4175
+ scheduled_date, due_date, order_in_day, workstream_id, taxonomies,
4176
+ reference_number, local_reference_number, is_archived
4177
+ FROM tasks
4178
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
4179
+ `).get(tenantId, workspaceId, requireText(source.entity_id, 'Source entity id'));
4180
+ if (!installed || Number(installed.is_archived || 0) === 1
4181
+ || !Number.isSafeInteger(Number(installed.reference_number))
4182
+ || Number(installed.reference_number) < 1
4183
+ || installed.local_reference_number !== null) {
4184
+ throw new Error('Task create recovery requires a non-tombstoned authoritative task baseline.');
4185
+ }
4186
+ const installedByField = {
4187
+ title: installed.title,
4188
+ description: installed.description,
4189
+ priority: installed.priority == null ? null : Number(installed.priority),
4190
+ complexity: installed.complexity == null ? null : Number(installed.complexity),
4191
+ type: installed.type,
4192
+ category: installed.category,
4193
+ assignee: installed.assignee,
4194
+ status: installed.status,
4195
+ scheduledDate: installed.scheduled_date ?? null,
4196
+ dueDate: installed.due_date ?? null,
4197
+ orderInDay: installed.order_in_day == null ? null : Number(installed.order_in_day),
4198
+ workstreamId: installed.workstream_id ?? null,
4199
+ taxonomies: (() => {
4200
+ try {
4201
+ return JSON.parse(String(installed.taxonomies || '{}'));
4202
+ }
4203
+ catch {
4204
+ throw new Error('Authoritative task taxonomy baseline is invalid.');
4205
+ }
4206
+ })(),
4207
+ };
4208
+ for (const [field, value] of Object.entries(task)) {
4209
+ if (canonicalWorkspaceSyncJson(installedByField[field])
4210
+ !== canonicalWorkspaceSyncJson(value)) {
4211
+ throw new WorkspaceSyncIdempotencyConflictError();
4212
+ }
4213
+ }
4214
+ const ownership = this.readWorkspaceDomainOwnership({
4215
+ tenantId,
4216
+ workspaceId,
4217
+ domain: 'task',
4218
+ entityId: requireText(source.entity_id, 'Source entity id'),
4219
+ });
4220
+ if (!ownership.owned || ownership.activeOperation) {
4221
+ throw new Error('Task create recovery requires its authoritative task baseline to settle first.');
4222
+ }
4223
+ }
4224
+ readTaskAttachmentLinksRecoveryIntent(source) {
4225
+ const payload = this.parseFrozenRecoveryPayload(source);
4226
+ const allowedPayloadKeys = new Set(['links', 'originActor']);
4227
+ if (Object.keys(payload).some((key) => !allowedPayloadKeys.has(key))
4228
+ || !Array.isArray(payload.links)) {
4229
+ throw new Error('Task attachment-link recovery payload is invalid.');
4230
+ }
4231
+ if (payload.originActor !== undefined)
4232
+ parseWorkspaceSyncV3OriginActor(payload.originActor);
4233
+ const identities = new Set();
4234
+ const links = payload.links.map((raw, index) => {
4235
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
4236
+ throw new Error('Task attachment-link recovery payload is invalid.');
4237
+ }
4238
+ const link = raw;
4239
+ if (Object.keys(link).some((key) => !TASK_ATTACHMENT_LINK_FIELDS.has(key))) {
4240
+ throw new Error('Task attachment-link recovery payload contains unsupported fields.');
4241
+ }
4242
+ const assetId = requireText(link.assetId, 'Task attachment-link asset id');
4243
+ if (link.assetId !== assetId) {
4244
+ throw new Error('Task attachment-link recovery payload is not canonical.');
4245
+ }
4246
+ const role = String(link.role || '');
4247
+ if (!['attachment', 'image', 'reference'].includes(role)
4248
+ || !Number.isSafeInteger(link.order)
4249
+ || Number(link.order) !== index) {
4250
+ throw new Error('Task attachment-link recovery payload is not canonical.');
4251
+ }
4252
+ const identity = `${assetId}\0${role}`;
4253
+ if (identities.has(identity)) {
4254
+ throw new Error('Task attachment-link recovery payload contains a duplicate identity.');
4255
+ }
4256
+ identities.add(identity);
4257
+ for (const field of ['caption', 'displayName', 'originalFilename']) {
4258
+ if (link[field] !== undefined && typeof link[field] !== 'string') {
4259
+ throw new Error('Task attachment-link recovery metadata is invalid.');
4260
+ }
4261
+ if (typeof link[field] === 'string'
4262
+ && TASK_ATTACHMENT_FORBIDDEN_METADATA_VALUE.test(link[field])) {
4263
+ throw new Error('Task attachment-link recovery metadata contains a forbidden local, inline, or remote URL value.');
4264
+ }
4265
+ }
4266
+ if (link.referenceNumber !== undefined && link.referenceNumber !== null
4267
+ && (!Number.isSafeInteger(link.referenceNumber) || Number(link.referenceNumber) < 1)) {
4268
+ throw new Error('Task attachment-link recovery reference number is invalid.');
4269
+ }
4270
+ if (link.referenceLabel !== undefined && link.referenceLabel !== null
4271
+ && typeof link.referenceLabel !== 'string') {
4272
+ throw new Error('Task attachment-link recovery reference label is invalid.');
4273
+ }
4274
+ return {
4275
+ assetId,
4276
+ role,
4277
+ order: Number(link.order),
4278
+ displayName: typeof link.displayName === 'string' ? link.displayName : null,
4279
+ };
4280
+ });
4281
+ return { links };
4282
+ }
4283
+ isTaskAttachmentLinksRecoveryIntentSatisfied(tenantId, workspaceId, source) {
4284
+ const intent = this.readTaskAttachmentLinksRecoveryIntent(source);
4285
+ const installed = this.db.prepare(`
4286
+ SELECT asset_id, role, sort_order, display_name
4287
+ FROM workspace_asset_links
4288
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id = ?
4289
+ AND target_type = 'task' AND deleted_at IS NULL
4290
+ ORDER BY sort_order, asset_id, role
4291
+ `).all(tenantId, workspaceId, requireText(source.entity_id, 'Source entity id')).map((link, index) => ({
4292
+ assetId: String(link.asset_id || ''),
4293
+ role: String(link.role || ''),
4294
+ order: Number.isSafeInteger(link.sort_order) && Number(link.sort_order) >= 0
4295
+ ? Number(link.sort_order)
4296
+ : index,
4297
+ displayName: typeof link.display_name === 'string' ? link.display_name : null,
4298
+ }));
4299
+ return canonicalWorkspaceSyncJson(installed) === canonicalWorkspaceSyncJson(intent.links);
4300
+ }
4301
+ readTaskCommentAppendRecoveryIntent(workspaceId, source) {
4302
+ let payload;
4303
+ try {
4304
+ payload = JSON.parse(String(source.payload_json || ''));
4305
+ }
4306
+ catch {
4307
+ throw new Error('Workspace sync recovery payload is not valid JSON.');
4308
+ }
4309
+ if (fingerprintWorkspaceSyncPayload(payload) !== source.payload_fingerprint) {
4310
+ throw new Error('Workspace sync recovery payload fingerprint does not match.');
4311
+ }
4312
+ const keys = Object.keys(payload).sort();
4313
+ const hasOriginActor = payload.originActor !== undefined;
4314
+ const expectedKeys = hasOriginActor ? ['comment', 'originActor'] : ['comment'];
4315
+ if (canonicalWorkspaceSyncJson(keys) !== canonicalWorkspaceSyncJson(expectedKeys)) {
4316
+ throw new Error('Task comment recovery payload contains unsupported fields.');
4317
+ }
4318
+ if (hasOriginActor)
4319
+ parseWorkspaceSyncV3OriginActor(payload.originActor);
4320
+ if (!payload.comment || typeof payload.comment !== 'object' || Array.isArray(payload.comment)) {
4321
+ throw new Error('Task comment recovery payload is invalid.');
4322
+ }
4323
+ const commentKeys = Object.keys(payload.comment).sort();
4324
+ if (canonicalWorkspaceSyncJson(commentKeys)
4325
+ !== canonicalWorkspaceSyncJson(['author', 'id', 'submittedAuthor', 'text', 'timestamp'])) {
4326
+ throw new Error('Task comment recovery payload is not canonical.');
4327
+ }
4328
+ const comments = normalizePlanningComments([payload.comment]);
4329
+ if (!comments || comments.length !== 1
4330
+ || canonicalWorkspaceSyncJson(comments[0]) !== canonicalWorkspaceSyncJson(payload.comment)) {
4331
+ throw new Error('Task comment recovery payload is not canonical.');
4332
+ }
4333
+ return {
4334
+ comment: comments[0],
4335
+ activityEventId: buildTaskCommentActivityEventId({
4336
+ workspaceId: requireText(workspaceId, 'Source workspace id'),
4337
+ clientId: requireText(source.client_id, 'Source client id'),
4338
+ operationId: requireText(source.operation_id, 'Source operation id'),
4339
+ taskId: requireText(source.entity_id, 'Source entity id'),
4340
+ commentId: comments[0].id,
4341
+ }),
4342
+ };
4343
+ }
4344
+ prepareTaskCommentAppendRecoveryPayload(tenantId, workspaceId, source) {
4345
+ const intent = this.readTaskCommentAppendRecoveryIntent(workspaceId, source);
4346
+ this.assertTaskCommentAppendRecoveryEventOwned(tenantId, workspaceId, source, intent);
4347
+ const replayPayload = {
4348
+ comments: [intent.comment],
4349
+ provenance: [{
4350
+ commentId: intent.comment.id,
4351
+ actor: {
4352
+ kind: 'authoritative-entity-event',
4353
+ eventId: intent.activityEventId,
4354
+ },
4355
+ }],
4356
+ };
4357
+ const payloadJson = canonicalWorkspaceSyncJson(replayPayload);
4358
+ return {
4359
+ payloadJson,
4360
+ payloadFingerprint: fingerprintWorkspaceSyncPayload(replayPayload),
4361
+ replayMutationKind: 'reconcile',
4362
+ workflowDependencyCoalesced: false,
4363
+ };
4364
+ }
4365
+ assertTaskCommentAppendRecoveryEventOwned(tenantId, workspaceId, source, intent = this.readTaskCommentAppendRecoveryIntent(workspaceId, source)) {
4366
+ const taskId = requireText(source.entity_id, 'Source entity id');
4367
+ const event = this.db.prepare(`
4368
+ SELECT entity_type, entity_id, action, actor, details_json
4369
+ FROM entity_events
4370
+ WHERE id = ? AND tenant_id = ? AND workspace_id = ?
4371
+ `).get(intent.activityEventId, tenantId, workspaceId);
4372
+ let details = null;
4373
+ try {
4374
+ const parsed = JSON.parse(String(event?.details_json || ''));
4375
+ details = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
4376
+ ? parsed
4377
+ : null;
4378
+ }
4379
+ catch {
4380
+ details = null;
4381
+ }
4382
+ const expectedDetails = {
4383
+ commentId: intent.comment.id,
4384
+ preview: intent.comment.text.slice(0, 160),
4385
+ submittedAuthor: String(intent.comment.submittedAuthor || intent.comment.author || '').trim(),
4386
+ };
4387
+ const ownership = this.readWorkspaceDomainOwnership({
4388
+ tenantId,
4389
+ workspaceId,
4390
+ domain: 'entity-event',
4391
+ entityId: intent.activityEventId,
4392
+ });
4393
+ if (!event
4394
+ || event.entity_type !== 'task'
4395
+ || event.entity_id !== taskId
4396
+ || event.action !== 'task-comment-added'
4397
+ || event.actor !== intent.comment.author
4398
+ || canonicalWorkspaceSyncJson(details) !== canonicalWorkspaceSyncJson(expectedDetails)
4399
+ || !ownership.owned
4400
+ || ownership.activeOperation) {
4401
+ throw new Error('Task comment recovery requires its authoritative entity-event adoption to settle first.');
4402
+ }
4403
+ }
4404
+ isTaskCommentAppendRecoveryIntentSatisfied(tenantId, workspaceId, source) {
4405
+ const intent = this.readTaskCommentAppendRecoveryIntent(workspaceId, source);
4406
+ const installed = this.db.prepare(`
4407
+ SELECT id, text, author, timestamp, submitted_author
4408
+ FROM comments
4409
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? AND id = ?
4410
+ `).get(tenantId, workspaceId, requireText(source.entity_id, 'Source entity id'), intent.comment.id);
4411
+ if (!installed)
4412
+ return false;
4413
+ const installedComments = normalizePlanningComments([{
4414
+ id: installed.id,
4415
+ text: installed.text,
4416
+ author: installed.author,
4417
+ timestamp: installed.timestamp,
4418
+ ...(installed.submitted_author == null
4419
+ ? {}
4420
+ : { submittedAuthor: installed.submitted_author }),
4421
+ }]);
4422
+ if (!installedComments || installedComments.length !== 1
4423
+ || canonicalWorkspaceSyncJson(installedComments[0])
4424
+ !== canonicalWorkspaceSyncJson(intent.comment)) {
4425
+ throw new WorkspaceSyncIdempotencyConflictError();
4426
+ }
4427
+ return true;
4428
+ }
4429
+ prepareTaskRecoveryPayload(tenantId, workspaceId, source) {
4430
+ let payload;
4431
+ try {
4432
+ payload = JSON.parse(String(source.payload_json || ''));
4433
+ }
4434
+ catch {
4435
+ throw new Error('Workspace sync recovery payload is not valid JSON.');
4436
+ }
4437
+ if (fingerprintWorkspaceSyncPayload(payload) !== source.payload_fingerprint) {
4438
+ throw new Error('Workspace sync recovery payload fingerprint does not match.');
4439
+ }
4440
+ if (source.mutation_kind !== 'set-status' || !payload.workflowAssignment) {
4441
+ return {
4442
+ payloadJson: canonicalWorkspaceSyncJson(payload),
4443
+ payloadFingerprint: fingerprintWorkspaceSyncPayload(payload),
4444
+ workflowDependencyCoalesced: false,
4445
+ };
4446
+ }
4447
+ const repairableCommentEvidenceConflict = (source.last_error_code === 'WORKFLOW_ASSIGNMENT_SYNC_CONFLICT'
4448
+ && source.last_error_message === 'Workflow assignment published steps conflict with canonical storage.');
4449
+ const normalizedAssignment = repairableCommentEvidenceConflict
4450
+ ? this.normalizeRecoveredWorkflowStepEvidence(tenantId, workspaceId, payload.workflowAssignment)
4451
+ : null;
4452
+ const workflowDefinitionNormalized = normalizedAssignment?.changed === true;
4453
+ if (workflowDefinitionNormalized) {
4454
+ payload.workflowAssignment = normalizedAssignment.assignment;
4455
+ }
4456
+ const requested = payload.workflowAssignment;
4457
+ const requestedExecution = requested.execution;
4458
+ const requestedEvents = Array.isArray(requested.events) ? requested.events : [];
4459
+ const executionId = String(requestedExecution?.id || '');
4460
+ if (!executionId
4461
+ || !requested.template || typeof requested.template !== 'object'
4462
+ || !requested.version || typeof requested.version !== 'object'
4463
+ || !Array.isArray(requested.steps)
4464
+ || !requestedExecution?.taskId
4465
+ || !requestedExecution.workflowTemplateId
4466
+ || !requestedExecution.workflowVersionId
4467
+ || requestedEvents.length === 0) {
4468
+ throw new Error('Workflow assignment recovery requires semantic history reconciliation.');
4469
+ }
4470
+ const accepted = this.db.prepare(`
4471
+ SELECT payload_json, authoritative_response_json
4472
+ FROM workspace_sync_outbox
4473
+ WHERE tenant_id = ? AND workspace_id = ?
4474
+ AND domain = 'task' AND entity_id = ?
4475
+ AND mutation_kind = 'set-status' AND status = 'accepted'
4476
+ AND enqueue_sequence < ?
4477
+ ORDER BY enqueue_sequence DESC
4478
+ `).all(tenantId, workspaceId, requireText(source.entity_id, 'Source entity id'), normalizeSequence(source.enqueue_sequence ?? 0));
4479
+ const immutableExecution = (execution) => ({
4480
+ id: execution?.id,
4481
+ taskId: execution?.taskId,
4482
+ workflowTemplateId: execution?.workflowTemplateId,
4483
+ workflowVersionId: execution?.workflowVersionId,
4484
+ });
4485
+ const dependencySatisfied = accepted.some((candidate) => {
4486
+ try {
4487
+ const acceptedPayload = JSON.parse(String(candidate.payload_json || ''));
4488
+ const acceptedResponse = JSON.parse(String(candidate.authoritative_response_json || ''));
4489
+ const acceptedRequest = acceptedPayload.workflowAssignment;
4490
+ const authoritative = acceptedResponse.workflowAssignment;
4491
+ const authoritativeExecution = authoritative?.execution;
4492
+ const authoritativeEvents = Array.isArray(authoritative?.events) ? authoritative.events : [];
4493
+ return String(acceptedRequest?.execution?.id || '')
4494
+ === executionId
4495
+ && canonicalWorkspaceSyncJson(requested.template)
4496
+ === canonicalWorkspaceSyncJson(authoritative?.template)
4497
+ && canonicalWorkspaceSyncJson(requested.version)
4498
+ === canonicalWorkspaceSyncJson(authoritative?.version)
4499
+ && canonicalWorkspaceSyncJson(requested.steps)
4500
+ === canonicalWorkspaceSyncJson(authoritative?.steps)
4501
+ && canonicalWorkspaceSyncJson(immutableExecution(requestedExecution))
4502
+ === canonicalWorkspaceSyncJson(immutableExecution(authoritativeExecution))
4503
+ && authoritativeExecution?.status !== 'pending'
4504
+ && authoritativeEvents.length >= requestedEvents.length
4505
+ && canonicalWorkspaceSyncJson(authoritativeEvents.slice(0, requestedEvents.length))
4506
+ === canonicalWorkspaceSyncJson(requestedEvents);
4507
+ }
4508
+ catch {
4509
+ return false;
4510
+ }
4511
+ });
4512
+ if (!dependencySatisfied) {
4513
+ if (workflowDefinitionNormalized) {
4514
+ return {
4515
+ payloadJson: canonicalWorkspaceSyncJson(payload),
4516
+ payloadFingerprint: fingerprintWorkspaceSyncPayload(payload),
4517
+ workflowDependencyCoalesced: false,
4518
+ };
4519
+ }
4520
+ throw new Error('Workflow assignment recovery requires semantic history reconciliation.');
4521
+ }
4522
+ delete payload.workflowAssignment;
4523
+ return {
4524
+ payloadJson: canonicalWorkspaceSyncJson(payload),
4525
+ payloadFingerprint: fingerprintWorkspaceSyncPayload(payload),
4526
+ workflowDependencyCoalesced: true,
4527
+ };
4528
+ }
4529
+ allocateSequenceRange(tenantId, workspaceId, now, count) {
4530
+ const row = this.db.prepare(`
4531
+ INSERT INTO workspace_sync_sequences (tenant_id, workspace_id, last_sequence, updated_at)
4532
+ VALUES (?, ?, ?, ?)
4533
+ ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
4534
+ last_sequence = workspace_sync_sequences.last_sequence + excluded.last_sequence,
4535
+ updated_at = excluded.updated_at
4536
+ RETURNING CAST(last_sequence AS TEXT) AS last_sequence
4537
+ `).get(tenantId, workspaceId, count, now);
4538
+ return normalizeSequence(row?.last_sequence);
4539
+ }
4540
+ allocateOutboxEnqueueSequence(tenantId, workspaceId, now) {
4541
+ const row = this.db.prepare(`
4542
+ INSERT INTO workspace_sync_outbox_sequences (tenant_id, workspace_id, last_sequence, updated_at)
4543
+ VALUES (?, ?, 1, ?)
4544
+ ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
4545
+ last_sequence = workspace_sync_outbox_sequences.last_sequence + 1,
4546
+ updated_at = excluded.updated_at
4547
+ RETURNING CAST(last_sequence AS TEXT) AS last_sequence
4548
+ `).get(tenantId, workspaceId, now);
4549
+ return normalizeSequence(row?.last_sequence);
4550
+ }
4551
+ readOperationRow(ids) {
4552
+ return this.db.prepare(`
4553
+ SELECT contract_version, request_fingerprint, status, response_json,
4554
+ CAST(accepted_sequence AS TEXT) AS accepted_sequence,
4555
+ lease_owner, lease_expires_at
4556
+ FROM workspace_sync_operation_results
4557
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
4558
+ `).get(...ids);
4559
+ }
4560
+ assertMatchingOperation(row, requestFingerprint) {
4561
+ if (!row)
4562
+ throw new Error('Workspace sync operation does not exist.');
4563
+ if (Number(row.contract_version) !== WORKSPACE_SYNC_CONTRACT_VERSION || row.request_fingerprint !== requestFingerprint) {
4564
+ throw new WorkspaceSyncIdempotencyConflictError();
4565
+ }
4566
+ }
4567
+ mapOutboxRow(row) {
4568
+ return {
4569
+ tenantId: requireText(row.tenant_id, 'Tenant id'),
4570
+ workspaceId: requireText(row.workspace_id, 'Workspace id'),
4571
+ clientId: requireText(row.client_id, 'Client id'),
4572
+ operationId: requireText(row.operation_id, 'Operation id'),
4573
+ dispatchClass: requireOutboxDispatchClass(row.dispatch_class),
4574
+ domain: requireText(row.domain, 'Domain'),
4575
+ entityId: requireText(row.entity_id, 'Entity id'),
4576
+ mutationKind: requireText(row.mutation_kind, 'Mutation kind'),
4577
+ ownsTaskMetadata: Number(row.owns_task_metadata) === 1,
4578
+ ownsTaskLifecycle: Number(row.owns_task_lifecycle) === 1,
4579
+ ownsTaskAssignee: Number(row.owns_task_assignee) === 1,
4580
+ baseEntityRevision: normalizeSequence(row.base_entity_revision),
4581
+ baseLifecycleRevision: normalizeSequence(row.base_lifecycle_revision),
4582
+ payloadJson: requireText(row.payload_json, 'Payload'),
4583
+ payloadFingerprint: requireText(row.payload_fingerprint, 'Payload fingerprint'),
4584
+ status: requireText(row.status, 'Outbox status'),
4585
+ attemptCount: Number(row.attempt_count || 0),
4586
+ initialNotFoundAttemptCount: Number(row.initial_not_found_attempt_count || 0),
4587
+ enqueueSequence: normalizeSequence(row.enqueue_sequence ?? 0),
4588
+ rebaseBeforeFirstAttempt: Number(row.rebase_before_first_attempt) === 1,
4589
+ workstreamMetadataRevisionRebaseCount: Number(row.workstream_metadata_revision_rebase_count || 0),
4590
+ initiativeMetadataRevisionRebaseCount: Number(row.initiative_metadata_revision_rebase_count || 0),
4591
+ leaseOwner: row.lease_owner == null ? null : String(row.lease_owner),
4592
+ leaseExpiresAt: row.lease_expires_at == null ? null : String(row.lease_expires_at),
4593
+ };
4594
+ }
4595
+ }
4596
+ // Consistent reservation lifecycle for a V3 mutation service: reserve, run
4597
+ // arbitrary caller work (readiness/identity checks, then an accept/commit
4598
+ // call), and release the reservation immediately if that work throws before
4599
+ // committing. Without this, a business-logic failure between reserveOperation
4600
+ // and acceptReservedOperationBatch/commitReservedOperationResponse leaves the
4601
+ // reservation row orphaned in status='processing' until its lease naturally
4602
+ // expires, so every retry sees WORKSPACE_SYNC_OPERATION_IN_PROGRESS instead of
4603
+ // re-attempting. acceptReservedOperationBatch and commitReservedOperationResponse
4604
+ // already self-release on a throw from inside their own transaction (the most
4605
+ // common case, ordinary business validation inside applyMutation); this helper
4606
+ // additionally covers a service's own pre-accept work, exactly like the
4607
+ // TASK_ATTACHMENT_ASSET_NOT_READY readiness check for task-attachment-links.
4608
+ // Never deletes, rewrites, or force-accepts the operation: on release, the
4609
+ // exact same operation identity is immediately eligible for reservation
4610
+ // takeover on the next call with the same request fingerprint.
4611
+ export function runReservedWorkspaceSyncV3Operation(input) {
4612
+ const reservation = input.store.reserveOperation({
4613
+ identity: input.identity,
4614
+ requestFingerprint: input.requestFingerprint,
4615
+ now: input.now,
4616
+ leaseExpiresAt: input.leaseExpiresAt,
4617
+ leaseOwner: input.leaseOwner,
4618
+ });
4619
+ if (reservation.state !== 'owned')
4620
+ return reservation;
4621
+ try {
4622
+ return input.attempt(reservation);
4623
+ }
4624
+ catch (error) {
4625
+ if (!(error instanceof WorkspaceSyncLeaseLostError)) {
4626
+ input.store.releaseFailedOperationReservation({
4627
+ identity: input.identity,
4628
+ requestFingerprint: input.requestFingerprint,
4629
+ leaseOwner: reservation.leaseOwner,
4630
+ now: input.now,
4631
+ });
4632
+ }
4633
+ throw error;
4634
+ }
4635
+ }