@taskforcehq/taskforce 0.3.328 → 0.3.329

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 (264) hide show
  1. package/dist/Taskforce.module.css +233 -34
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  8. package/dist/components/features/TaskSettings.d.ts +12 -1
  9. package/dist/components/features/TaskSettings.js +147 -40
  10. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  11. package/dist/components/features/planning/PlanningModule.js +2 -5
  12. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  13. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  14. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  15. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  16. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  17. package/dist/components/task/TaskCard.d.ts +5 -0
  18. package/dist/components/task/TaskCard.js +65 -10
  19. package/dist/components/task/TaskImageReviews.js +14 -5
  20. package/dist/components/task/TaskKanban.d.ts +6 -1
  21. package/dist/components/task/TaskKanban.js +152 -6
  22. package/dist/components/task/TaskList.d.ts +5 -0
  23. package/dist/components/task/TaskList.js +3 -3
  24. package/dist/components/ui/PasswordInput.d.ts +6 -0
  25. package/dist/components/ui/PasswordInput.js +11 -0
  26. package/dist/components/ui/TopNoticeLayer.js +1 -1
  27. package/dist/components/views/AppShellHeader.js +32 -11
  28. package/dist/components/views/PlanComparisonPage.js +12 -0
  29. package/dist/components/views/StandaloneLayout.js +7 -31
  30. package/dist/components/views/WidgetView.js +3 -3
  31. package/dist/components/views/panels/PlanningDrawer.js +5 -12
  32. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  33. package/dist/config/envSchema.js +12 -0
  34. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  35. package/dist/config/localServiceRuntimeContract.js +135 -0
  36. package/dist/core/AiProfileService.js +46 -8
  37. package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
  38. package/dist/core/DeletedTaskLifecycleService.js +37 -6
  39. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
  40. package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
  41. package/dist/core/Taskforce.d.ts +62 -2
  42. package/dist/core/Taskforce.js +635 -32
  43. package/dist/core/types.d.ts +6 -0
  44. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  45. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  46. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  47. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  48. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  49. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  50. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  51. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  52. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  53. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  54. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  55. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  56. package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
  57. package/dist/hooks/tasks/useTaskFormActions.js +65 -12
  58. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  59. package/dist/hooks/ui/useSettingsModel.js +13 -2
  60. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  61. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  62. package/dist/hooks/useFilterSort.d.ts +7 -3
  63. package/dist/hooks/useFilterSort.js +15 -5
  64. package/dist/hooks/useTaskData.d.ts +10 -1
  65. package/dist/hooks/useTaskData.js +216 -7
  66. package/dist/hooks/useTaskMutations.d.ts +10 -2
  67. package/dist/hooks/useTaskMutations.js +223 -43
  68. package/dist/hooks/useTaskforce.d.ts +37 -9
  69. package/dist/hooks/useTaskforce.js +349 -39
  70. package/dist/hooks/useUiNotice.d.ts +4 -0
  71. package/dist/hooks/useUiNotice.js +4 -0
  72. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  73. package/dist/mcp/documentAssetRegistrar.js +44 -35
  74. package/dist/mcp/documentHelpers.d.ts +1 -0
  75. package/dist/mcp/documentHelpers.js +7 -3
  76. package/dist/mcp/runtime.js +172 -42
  77. package/dist/mcp/taskPlanningRegistrar.js +41 -38
  78. package/dist/mcp/toolCallCompatibility.js +38 -1
  79. package/dist/mcp/toolCatalog.d.ts +1 -0
  80. package/dist/migrations/taskSchemaMigrations.js +12 -0
  81. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  82. package/dist/runtime/appGateDefinitions.js +6 -6
  83. package/dist/runtime/appGates.d.ts +12 -13
  84. package/dist/runtime/appGates.js +29 -13
  85. package/dist/runtime/nodeAppGates.d.ts +8 -0
  86. package/dist/runtime/nodeAppGates.js +27 -0
  87. package/dist/server/cloudCollectionRead.d.ts +1 -1
  88. package/dist/server/cloudCollectionRead.js +2 -2
  89. package/dist/server/index.d.ts +3 -0
  90. package/dist/server/index.js +15 -3
  91. package/dist/server/localServiceLease.d.ts +3 -0
  92. package/dist/server/localServiceLease.js +9 -1
  93. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  94. package/dist/server/rateLimit.d.ts +1 -0
  95. package/dist/server/rateLimit.js +4 -0
  96. package/dist/server/realtimeSyncWs.js +36 -12
  97. package/dist/server/routes/admin.js +1 -0
  98. package/dist/server/routes/agentRoles.js +3 -2
  99. package/dist/server/routes/agentSkills.js +3 -2
  100. package/dist/server/routes/agents.js +118 -16
  101. package/dist/server/routes/annotatedAttachments.js +3 -2
  102. package/dist/server/routes/auth.js +3 -1
  103. package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
  104. package/dist/server/routes/localService.js +2 -0
  105. package/dist/server/routes/primitives.d.ts +2 -0
  106. package/dist/server/routes/syncAuxRoutes.js +5 -2
  107. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  108. package/dist/server/routes/syncV3FeedRoutes.js +18 -16
  109. package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
  110. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  111. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  112. package/dist/server/routes/syncV3OperationRoutes.js +10 -0
  113. package/dist/server/routes/tasks.js +103 -11
  114. package/dist/server/routes/workspaces.js +43 -1
  115. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  116. package/dist/server/workspaceDeletionAdmission.js +50 -0
  117. package/dist/service/localServiceCli.js +16 -0
  118. package/dist/service/localServiceClientLifecycle.js +36 -10
  119. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  120. package/dist/services/codexAppServerModelGateway.js +438 -0
  121. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  122. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  123. package/dist/shared/taskforceAgentDefinition.js +7 -1
  124. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  125. package/dist/shared/taskforceAgentModels.js +30 -1
  126. package/dist/storage/postgresAdapter.d.ts +20 -3
  127. package/dist/storage/postgresAdapter.js +106 -14
  128. package/dist/storage/postgresWorker.js +13 -4
  129. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  130. package/dist/storage/postgresWorkerProtocol.js +30 -0
  131. package/dist/sync/cloudSyncApi.d.ts +4 -1
  132. package/dist/sync/cloudSyncApi.js +7 -2
  133. package/dist/sync/contentSyncState.d.ts +1 -1
  134. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
  135. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
  136. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  137. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  138. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  139. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  140. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
  141. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  142. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
  143. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  144. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
  145. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  146. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  147. package/dist/sync/syncService.d.ts +2 -0
  148. package/dist/sync/syncService.js +4 -1
  149. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  150. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
  151. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
  152. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  153. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  154. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  155. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
  156. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  157. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  158. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
  159. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
  160. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  161. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  162. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  163. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  164. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  165. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  166. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  167. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  168. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  169. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  170. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  171. package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
  172. package/dist/sync/v3/syncDurabilityStore.js +31 -11
  173. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
  174. package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
  175. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  176. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  177. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  178. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  179. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  180. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  181. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  182. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  183. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  184. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  185. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  186. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  187. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  188. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
  189. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  190. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  191. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  192. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  193. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
  194. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
  195. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
  196. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
  197. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
  198. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
  199. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
  200. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  201. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
  202. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  203. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  204. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  205. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  206. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  207. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  208. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  209. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  210. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  211. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  212. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  213. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  214. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  215. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  216. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
  217. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  218. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  219. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  220. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  221. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  222. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  223. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  224. package/dist/sync/workspacePullFeed.js +2 -1
  225. package/dist/sync/workspaceRepair.d.ts +3 -0
  226. package/dist/sync/workspaceRepair.js +67 -6
  227. package/dist/types.d.ts +47 -1
  228. package/dist/types.js +6 -0
  229. package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  230. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  231. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  232. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  233. package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
  234. package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
  235. package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
  236. package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
  237. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  238. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  239. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  240. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  241. package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  242. package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
  243. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  244. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  245. package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
  246. package/dist/ui/index.html +3 -3
  247. package/dist/utils/taskActivity.d.ts +3 -2
  248. package/dist/utils/taskActivity.js +19 -2
  249. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  250. package/dist/utils/taskEditorReconciliation.js +217 -0
  251. package/dist/utils/taskNormalization.d.ts +1 -0
  252. package/dist/utils/taskNormalization.js +39 -0
  253. package/dist/utils/taskSearch.js +2 -2
  254. package/dist/utils/uiNotice.d.ts +4 -0
  255. package/package.json +16 -4
  256. package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
  257. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  258. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
  259. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
  260. package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
  261. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
  262. package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
  263. package/dist/ui/assets/index-BUplSsv_.js +0 -7
  264. package/dist/ui/assets/index-gz2EXuoK.css +0 -1
@@ -5,7 +5,7 @@ import { validateTaskActivityAuthoritativeRevision } from './taskActivityAuthori
5
5
  import { serializeEntityActivityForSync, serializeTaskActivityForSync, WORKSPACE_SYNC_ENTITY_EVENT_APPEND, WORKSPACE_SYNC_ENTITY_EVENT_DOMAIN, } from './taskActivityJournal.js';
6
6
  import { fingerprintWorkspaceSyncPayload, } from './syncDurabilityStore.js';
7
7
  import { serializeTaskRootForSync } from './workspaceSyncV3TaskRootProjection.js';
8
- import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, validateWorkspaceSyncV3TaskAttachmentLinksPayload, validateWorkspaceSyncV3TaskChecklistPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
8
+ import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, validateWorkspaceSyncV3TaskAttachmentLinksPayload, validateWorkspaceSyncV3TaskAttachmentLinksV2Payload, validateWorkspaceSyncV3TaskChecklistPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
9
9
  export class LocalAuthoritativeTaskCreateError extends Error {
10
10
  code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID';
11
11
  constructor(message) {
@@ -264,7 +264,9 @@ export function applyAuthoritativeTaskCreateAndAck(input) {
264
264
  throw new LocalAuthoritativeTaskCreateError('Authoritative task create attachment-link projection is invalid.');
265
265
  }
266
266
  try {
267
- canonicalAttachmentProjection = validateWorkspaceSyncV3TaskAttachmentLinksPayload(attachmentProjection.payload);
267
+ canonicalAttachmentProjection = Object.prototype.hasOwnProperty.call(attachmentProjection.payload, 'cardCoverAssetId')
268
+ ? validateWorkspaceSyncV3TaskAttachmentLinksV2Payload(attachmentProjection.payload)
269
+ : validateWorkspaceSyncV3TaskAttachmentLinksPayload(attachmentProjection.payload);
268
270
  }
269
271
  catch {
270
272
  throw new LocalAuthoritativeTaskCreateError('Authoritative task create attachment-link payload is invalid.');
@@ -272,10 +274,16 @@ export function applyAuthoritativeTaskCreateAndAck(input) {
272
274
  if (canonicalAttachmentProjection.taskId !== task.id) {
273
275
  throw new LocalAuthoritativeTaskCreateError('Authoritative task create attachment-link identity is invalid.');
274
276
  }
275
- const capturedAttachmentProjection = serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
276
- taskId: task.id,
277
- links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
278
- });
277
+ const capturedAttachmentProjection = 'cardCoverAssetId' in canonicalAttachmentProjection
278
+ ? serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload({
279
+ taskId: task.id,
280
+ links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
281
+ cardCoverAssetId: task.cardCoverAssetId ?? null,
282
+ })
283
+ : serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
284
+ taskId: task.id,
285
+ links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
286
+ });
279
287
  const relationshipIntent = (projection) => ({
280
288
  taskId: projection.taskId,
281
289
  links: projection.links.map(({ assetId, role, order, displayName }) => ({
@@ -284,6 +292,7 @@ export function applyAuthoritativeTaskCreateAndAck(input) {
284
292
  order,
285
293
  ...(displayName ? { displayName } : {}),
286
294
  })),
295
+ ...('cardCoverAssetId' in projection ? { cardCoverAssetId: projection.cardCoverAssetId } : {}),
287
296
  });
288
297
  if (fingerprintWorkspaceSyncPayload(relationshipIntent(canonicalAttachmentProjection))
289
298
  !== fingerprintWorkspaceSyncPayload(relationshipIntent(capturedAttachmentProjection))) {
@@ -7,6 +7,7 @@ export declare function createLocalTaskMetadataClient(input?: LocalTaskOperation
7
7
  entityId: string;
8
8
  patch: Partial<TaskItem>;
9
9
  saveSource?: "manual" | "autosave";
10
+ expectedDetailRevision?: string;
10
11
  isAuthenticated: boolean;
11
12
  dispatchEnabled?: boolean;
12
13
  }): Promise<LocalTaskMetadataMutationResult>;
@@ -15,6 +15,7 @@ export function createLocalTaskMetadataClient(input = {}) {
15
15
  payload: {
16
16
  patch,
17
17
  ...(inputValue.saveSource ? { saveSource: inputValue.saveSource } : {}),
18
+ ...(inputValue.expectedDetailRevision ? { expectedDetailRevision: inputValue.expectedDetailRevision } : {}),
18
19
  },
19
20
  };
20
21
  return client.execute({
@@ -18,12 +18,15 @@ function buildTaskMetadataEnvelope(record) {
18
18
  throw new Error('Local outbox payload must be an object.');
19
19
  }
20
20
  const payloadRecord = payload;
21
- const allowedKeys = new Set(['patch', 'saveSource', 'originActor']);
21
+ const allowedKeys = new Set(['patch', 'saveSource', 'expectedDetailRevision', 'originActor']);
22
22
  if (!Object.prototype.hasOwnProperty.call(payloadRecord, 'patch')
23
23
  || Object.keys(payloadRecord).some((key) => !allowedKeys.has(key))) {
24
24
  throw new Error('Local task metadata outbox payload must contain patch and optional saveSource/originActor.');
25
25
  }
26
26
  const saveSource = payloadRecord.saveSource;
27
+ const expectedDetailRevision = typeof payloadRecord.expectedDetailRevision === 'string'
28
+ ? payloadRecord.expectedDetailRevision
29
+ : undefined;
27
30
  if (saveSource !== undefined && saveSource !== 'manual' && saveSource !== 'autosave') {
28
31
  throw new Error('Local task metadata outbox saveSource must be manual or autosave.');
29
32
  }
@@ -41,6 +44,7 @@ function buildTaskMetadataEnvelope(record) {
41
44
  payload: {
42
45
  patch: validateDurableTaskMetadataPatch(payloadRecord.patch),
43
46
  ...(saveSource ? { saveSource } : {}),
47
+ ...(expectedDetailRevision ? { expectedDetailRevision } : {}),
44
48
  ...(payloadRecord.originActor !== undefined
45
49
  ? { originActor: parseWorkspaceSyncV3OriginActor(payloadRecord.originActor) }
46
50
  : {}),
@@ -17,6 +17,7 @@ export interface LocalTaskMetadataOutboxCore {
17
17
  allowAuthoritativeReplacement?: boolean;
18
18
  }): unknown;
19
19
  notifyDurableTaskMetadataCommitted(workspaceId: string, taskId: string): void;
20
+ assertTaskDetailRevisionForUpdate?(id: string, expectedDetailRevision: string, workspaceId?: string): void;
20
21
  }
21
22
  export declare class LocalAuthoritativeTaskMetadataError extends Error {
22
23
  readonly code = "WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID";
@@ -29,6 +30,7 @@ export declare function enqueueLocalTaskMetadataMutation(input: {
29
30
  taskId: string;
30
31
  patch: Partial<TaskItem>;
31
32
  saveSource?: 'manual' | 'autosave';
33
+ expectedDetailRevision?: string;
32
34
  now: string;
33
35
  actorRef: string;
34
36
  requireV3CombinedOwnership?: boolean;
@@ -30,10 +30,17 @@ export function enqueueLocalTaskMetadataMutation(input) {
30
30
  payload: withOriginActorPayload({
31
31
  patch,
32
32
  ...(input.saveSource ? { saveSource: input.saveSource } : {}),
33
+ ...(input.expectedDetailRevision ? { expectedDetailRevision: input.expectedDetailRevision } : {}),
33
34
  }, input.actorRef),
34
35
  ...(input.requireV3CombinedOwnership ? { requireV3PlanningOwnership: 'combined' } : {}),
35
36
  now: input.now,
36
37
  applyLocalMutation: () => {
38
+ if (input.expectedDetailRevision) {
39
+ if (!input.core.assertTaskDetailRevisionForUpdate) {
40
+ throw new DurableTaskMetadataMutationError('Task detail revision checks are unavailable.', 'DURABLE_TASK_PATCH_INVALID', 500);
41
+ }
42
+ input.core.assertTaskDetailRevisionForUpdate(input.taskId, input.expectedDetailRevision, input.identity.workspaceId);
43
+ }
37
44
  const task = input.core.updateTask(input.taskId, patch, input.identity.workspaceId, {
38
45
  persistBackup: false,
39
46
  emitRealtime: false,
@@ -5,7 +5,7 @@ import { buildTaskLifecycleActivityEventId } from './taskLifecycleActivityIdenti
5
5
  import { validateTaskActivityAuthoritativeRevision } from './taskActivityAuthoritativeRevision.js';
6
6
  import { serializeTaskActivityForSync, WORKSPACE_SYNC_ENTITY_EVENT_APPEND, WORKSPACE_SYNC_ENTITY_EVENT_DOMAIN, } from './taskActivityJournal.js';
7
7
  import { fingerprintWorkspaceSyncPayload, WorkspaceSyncIdempotencyConflictError, } from './syncDurabilityStore.js';
8
- import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, validateWorkspaceSyncV3TaskAttachmentLinksPayload, validateWorkspaceSyncV3TaskChecklistPayload, validateWorkspaceSyncV3TaskCommentsPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
8
+ import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, validateWorkspaceSyncV3TaskAttachmentLinksV2Payload, validateWorkspaceSyncV3TaskAttachmentLinksPayload, validateWorkspaceSyncV3TaskChecklistPayload, validateWorkspaceSyncV3TaskCommentsPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
9
9
  export class LocalAuthoritativeTaskRestoreError extends Error {
10
10
  code = 'WORKSPACE_SYNC_AUTHORITATIVE_RESPONSE_INVALID';
11
11
  constructor(message) {
@@ -190,7 +190,9 @@ export function applyAuthoritativeTaskRestoreAndAck(input) {
190
190
  let canonicalAttachments;
191
191
  try {
192
192
  canonicalChecklist = validateWorkspaceSyncV3TaskChecklistPayload(checklist.payload);
193
- canonicalAttachments = validateWorkspaceSyncV3TaskAttachmentLinksPayload(attachments.payload);
193
+ canonicalAttachments = Object.prototype.hasOwnProperty.call(attachments.payload, 'cardCoverAssetId')
194
+ ? validateWorkspaceSyncV3TaskAttachmentLinksV2Payload(attachments.payload)
195
+ : validateWorkspaceSyncV3TaskAttachmentLinksPayload(attachments.payload);
194
196
  }
195
197
  catch {
196
198
  throw new LocalAuthoritativeTaskRestoreError('Authoritative task restore nested projection is invalid.');
@@ -214,10 +216,16 @@ export function applyAuthoritativeTaskRestoreAndAck(input) {
214
216
  !== fingerprintWorkspaceSyncPayload(serializeWorkspaceSyncV3TaskChecklistPayload({ taskId: task.id, items: task.checklistItems || [] }))) {
215
217
  throw new LocalAuthoritativeTaskRestoreError('Authoritative task restore projections do not match the task payload.');
216
218
  }
217
- const capturedAttachments = serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
218
- taskId: task.id,
219
- links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
220
- });
219
+ const capturedAttachments = 'cardCoverAssetId' in canonicalAttachments
220
+ ? serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload({
221
+ taskId: task.id,
222
+ links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
223
+ cardCoverAssetId: task.cardCoverAssetId ?? null,
224
+ })
225
+ : serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
226
+ taskId: task.id,
227
+ links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.identity.workspaceId),
228
+ });
221
229
  if (fingerprintWorkspaceSyncPayload(canonicalAttachments)
222
230
  !== fingerprintWorkspaceSyncPayload(capturedAttachments)) {
223
231
  throw new LocalAuthoritativeTaskRestoreError('Authoritative task restore attachment links do not match the captured projection.');
@@ -9,6 +9,7 @@ export type LocalTaskStatusMutationInput = {
9
9
  assigneeIntent: DurableTaskStatusAssigneeIntent;
10
10
  canceledReason: string | null;
11
11
  metadataPatch?: Partial<TaskItem>;
12
+ expectedDetailRevision?: string;
12
13
  isAuthenticated: boolean;
13
14
  dispatchEnabled?: boolean;
14
15
  };
@@ -20,7 +20,12 @@ export function createLocalTaskStatusClient(input = {}) {
20
20
  domain: 'task',
21
21
  entityId,
22
22
  mutationKind: 'set-status',
23
- payload,
23
+ payload: {
24
+ ...payload,
25
+ ...(inputValue.expectedDetailRevision
26
+ ? { expectedDetailRevision: inputValue.expectedDetailRevision }
27
+ : {}),
28
+ },
24
29
  };
25
30
  return client.execute({
26
31
  workspaceId,
@@ -3,6 +3,7 @@ import { type DurableTaskStatusMutationCore, type DurableTaskStatusMutationPaylo
3
3
  import { WorkspaceSyncDurabilityStore, type WorkspaceSyncOperationIdentity, type WorkspaceSyncOutboxRecord } from './syncDurabilityStore.js';
4
4
  import type { DatabaseAdapter } from '../../storage/databaseAdapter.js';
5
5
  export interface LocalTaskStatusOutboxCore extends DurableTaskStatusMutationCore {
6
+ assertTaskDetailRevisionForUpdate?(id: string, expectedDetailRevision: string, workspaceId?: string): void;
6
7
  getDatabaseAdapter(): DatabaseAdapter;
7
8
  getTenantId(): string;
8
9
  installDurableTaskStatusAuthoritativeForSync(task: TaskItem, payload: DurableTaskStatusMutationPayload, installAssignee: boolean, workspaceId: string): TaskItem;
@@ -33,6 +34,7 @@ export declare function enqueueLocalTaskStatusMutation(input: {
33
34
  identity: WorkspaceSyncOperationIdentity;
34
35
  taskId: string;
35
36
  payload: unknown;
37
+ expectedDetailRevision?: string;
36
38
  now: string;
37
39
  actorRef: string;
38
40
  requireV3CombinedOwnership?: boolean;
@@ -74,23 +74,42 @@ export function enqueueLocalTaskStatusMutation(input) {
74
74
  taskLifecycle: true,
75
75
  taskAssignee: payload.assigneeIntent.kind !== 'preserve',
76
76
  },
77
- payload: withOriginActorPayload({ ...payload }, input.actorRef),
77
+ payload: withOriginActorPayload({
78
+ ...payload,
79
+ ...(input.expectedDetailRevision
80
+ ? { expectedDetailRevision: input.expectedDetailRevision }
81
+ : {}),
82
+ }, input.actorRef),
78
83
  ...(payload.workflowAssignment ? {
79
84
  coalesceWorkflowAssignmentExecutionId: payload.workflowAssignment.execution.id,
80
85
  } : {}),
81
86
  ...(input.requireV3CombinedOwnership ? { requireV3PlanningOwnership: 'combined' } : {}),
82
87
  now: input.now,
83
- applyLocalMutation: () => applyTaskStatusMutationForDurableSync({
84
- core: input.core,
85
- workspaceId: input.identity.workspaceId,
86
- taskId,
87
- targetStatus: statusPayload.status,
88
- metadataPatch: statusPayload.metadataPatch,
89
- assigneeIntent: statusPayload.assigneeIntent,
90
- canceledReason: statusPayload.canceledReason,
91
- updatedAt: input.now,
92
- actorRef: input.actorRef,
93
- taskEventId: activityEventId,
88
+ applyLocalMutation: () => {
89
+ if (input.expectedDetailRevision) {
90
+ if (!input.core.assertTaskDetailRevisionForUpdate) {
91
+ throw new Error('Task detail revision checks are unavailable.');
92
+ }
93
+ input.core.assertTaskDetailRevisionForUpdate(taskId, input.expectedDetailRevision, input.identity.workspaceId);
94
+ }
95
+ return applyTaskStatusMutationForDurableSync({
96
+ core: input.core,
97
+ workspaceId: input.identity.workspaceId,
98
+ taskId,
99
+ targetStatus: statusPayload.status,
100
+ metadataPatch: statusPayload.metadataPatch,
101
+ assigneeIntent: statusPayload.assigneeIntent,
102
+ canceledReason: statusPayload.canceledReason,
103
+ updatedAt: input.now,
104
+ actorRef: input.actorRef,
105
+ taskEventId: activityEventId,
106
+ });
107
+ },
108
+ buildCanonicalIntent: (result) => ({
109
+ metadataPatch: Object.fromEntries(Object.keys(payload.metadataPatch).map((key) => [
110
+ key,
111
+ result.task[key] ?? null,
112
+ ])),
94
113
  }),
95
114
  });
96
115
  if (!result.enqueued)
@@ -189,9 +208,31 @@ export function applyAuthoritativeTaskStatusAndAck(input) {
189
208
  const localCanonical = input.core.getTask(task.id, input.identity.workspaceId);
190
209
  if (!localCanonical)
191
210
  throw new LocalAuthoritativeTaskStatusError('Local task status projection is missing.');
211
+ let canonicalMetadataIntent = null;
212
+ if (input.record.canonicalIntentJson) {
213
+ try {
214
+ const parsed = JSON.parse(input.record.canonicalIntentJson);
215
+ canonicalMetadataIntent = parsed.metadataPatch
216
+ && typeof parsed.metadataPatch === 'object'
217
+ && !Array.isArray(parsed.metadataPatch)
218
+ ? parsed.metadataPatch
219
+ : null;
220
+ }
221
+ catch {
222
+ canonicalMetadataIntent = null;
223
+ }
224
+ if (!canonicalMetadataIntent) {
225
+ throw new LocalAuthoritativeTaskStatusError('Persisted canonical task status intent is invalid.');
226
+ }
227
+ }
192
228
  for (const key of Object.keys(payload.metadataPatch)) {
229
+ if (canonicalMetadataIntent && !Object.prototype.hasOwnProperty.call(canonicalMetadataIntent, key)) {
230
+ throw new LocalAuthoritativeTaskStatusError('Persisted canonical task status intent is incomplete.');
231
+ }
193
232
  const actual = task[key];
194
- const expected = localCanonical[key];
233
+ const expected = canonicalMetadataIntent
234
+ ? canonicalMetadataIntent[key]
235
+ : localCanonical[key];
195
236
  if (fingerprintWorkspaceSyncPayload(actual ?? null) !== fingerprintWorkspaceSyncPayload(expected ?? null)) {
196
237
  throw new LocalAuthoritativeTaskStatusError('Authoritative task metadata does not match the persisted request.');
197
238
  }
@@ -80,6 +80,7 @@ export type WorkspaceSyncOutboxRecord = WorkspaceSyncOperationIdentity & {
80
80
  baseLifecycleRevision: string;
81
81
  payloadJson: string;
82
82
  payloadFingerprint: string;
83
+ canonicalIntentJson: string | null;
83
84
  status: string;
84
85
  attemptCount: number;
85
86
  initialNotFoundAttemptCount: number;
@@ -542,6 +543,7 @@ export declare class WorkspaceSyncDurabilityStore {
542
543
  queuedTaskSuccessor?: boolean;
543
544
  queuedWorkflowRuntimeSuccessor?: boolean;
544
545
  coalesceWorkflowAssignmentExecutionId?: string;
546
+ buildCanonicalIntent?: (mutationResult: T) => unknown;
545
547
  }): {
546
548
  enqueued: boolean;
547
549
  mutationResult?: T;
@@ -760,6 +762,11 @@ export declare class WorkspaceSyncDurabilityStore {
760
762
  private assertTaskCommentAppendRecoveryEventOwned;
761
763
  private isTaskCommentAppendRecoveryIntentSatisfied;
762
764
  private prepareTaskRecoveryPayload;
765
+ lockWorkspaceSequenceForMutation(input: {
766
+ tenantId: string;
767
+ workspaceId: string;
768
+ now: string;
769
+ }): void;
763
770
  private allocateSequenceRange;
764
771
  private allocateOutboxEnqueueSequence;
765
772
  private readOperationRow;
@@ -791,7 +791,7 @@ export class WorkspaceSyncDurabilityStore {
791
791
  mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
792
792
  CAST(base_entity_revision AS TEXT) AS base_entity_revision,
793
793
  CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
794
- payload_json, payload_fingerprint, status, attempt_count,
794
+ payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
795
795
  initial_not_found_attempt_count,
796
796
  CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
797
797
  rebase_before_first_attempt, lease_owner, lease_expires_at
@@ -847,7 +847,7 @@ export class WorkspaceSyncDurabilityStore {
847
847
  mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
848
848
  CAST(base_entity_revision AS TEXT) AS base_entity_revision,
849
849
  CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
850
- payload_json, payload_fingerprint, status, attempt_count,
850
+ payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
851
851
  initial_not_found_attempt_count,
852
852
  CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
853
853
  rebase_before_first_attempt, lease_owner, lease_expires_at
@@ -1028,7 +1028,7 @@ export class WorkspaceSyncDurabilityStore {
1028
1028
  mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1029
1029
  CAST(base_entity_revision AS TEXT) AS base_entity_revision,
1030
1030
  CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
1031
- payload_json, payload_fingerprint, status, attempt_count,
1031
+ payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
1032
1032
  initial_not_found_attempt_count,
1033
1033
  CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
1034
1034
  rebase_before_first_attempt, lease_owner, lease_expires_at
@@ -1396,7 +1396,8 @@ export class WorkspaceSyncDurabilityStore {
1396
1396
  const recoverySources = this.db.prepare(`
1397
1397
  SELECT client_id, operation_id, dispatch_class, domain, entity_id, mutation_kind,
1398
1398
  owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1399
- payload_json, payload_fingerprint, last_error_code, last_error_message,
1399
+ payload_json, payload_fingerprint, canonical_intent_json,
1400
+ last_error_code, last_error_message,
1400
1401
  EXISTS (
1401
1402
  SELECT 1
1402
1403
  FROM workspace_sync_outbox AS replay_candidate
@@ -1646,18 +1647,18 @@ export class WorkspaceSyncDurabilityStore {
1646
1647
  dispatch_class, domain, entity_id, mutation_kind,
1647
1648
  owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
1648
1649
  base_entity_revision, base_lifecycle_revision,
1649
- payload_json, payload_fingerprint, status, attempt_count,
1650
+ payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
1650
1651
  initial_not_found_attempt_count, enqueue_sequence, rebase_before_first_attempt,
1651
1652
  next_attempt_at, recovery_batch_id,
1652
1653
  replay_of_client_id, replay_of_operation_id, created_at, updated_at
1653
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, 0, ?, 1,
1654
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, 0, ?, 1,
1654
1655
  ?, ?, ?, ?, ?, ?)
1655
1656
  ON CONFLICT (tenant_id, workspace_id, client_id, operation_id) DO NOTHING
1656
- `).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), replayEntityId, 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);
1657
+ `).run(tenantId, workspaceId, operationId, WORKSPACE_SYNC_CONTRACT_VERSION, sourceClientId, requireOutboxDispatchClass(source.dispatch_class), requireText(source.domain, 'Source domain'), replayEntityId, 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, source.canonical_intent_json == null ? null : String(source.canonical_intent_json), enqueueSequence, now, recoveryBatchId, sourceClientId, sourceOperationId, now, now);
1657
1658
  this.db.prepare(`
1658
1659
  UPDATE workspace_sync_outbox
1659
1660
  SET base_entity_revision = ?, base_lifecycle_revision = ?,
1660
- payload_json = ?, payload_fingerprint = ?,
1661
+ payload_json = ?, payload_fingerprint = ?, canonical_intent_json = ?,
1661
1662
  status = 'pending', attempt_count = 0, initial_not_found_attempt_count = 0,
1662
1663
  rebase_before_first_attempt = 1, next_attempt_at = ?,
1663
1664
  lease_owner = NULL, lease_expires_at = NULL,
@@ -1667,7 +1668,7 @@ export class WorkspaceSyncDurabilityStore {
1667
1668
  WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
1668
1669
  AND recovery_batch_id = ? AND replay_of_client_id = ? AND replay_of_operation_id = ?
1669
1670
  AND status = 'superseded'
1670
- `).run(primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, now, now, tenantId, workspaceId, sourceClientId, operationId, recoveryBatchId, sourceClientId, sourceOperationId);
1671
+ `).run(primary.revision.entityRevision, primary.revision.lifecycleRevision, replayPayload.payloadJson, replayPayload.payloadFingerprint, source.canonical_intent_json == null ? null : String(source.canonical_intent_json), now, now, tenantId, workspaceId, sourceClientId, operationId, recoveryBatchId, sourceClientId, sourceOperationId);
1671
1672
  if (replayPayload.workflowDependencyCoalesced) {
1672
1673
  this.db.prepare(`
1673
1674
  UPDATE workspace_sync_outbox
@@ -2761,7 +2762,20 @@ export class WorkspaceSyncDurabilityStore {
2761
2762
  for (const target of targets) {
2762
2763
  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);
2763
2764
  }
2764
- return { enqueued: true, mutationResult: input.applyLocalMutation() };
2765
+ const mutationResult = input.applyLocalMutation();
2766
+ if (input.buildCanonicalIntent) {
2767
+ const canonicalIntentJson = canonicalWorkspaceSyncJson(input.buildCanonicalIntent(mutationResult));
2768
+ const updated = this.db.prepare(`
2769
+ UPDATE workspace_sync_outbox
2770
+ SET canonical_intent_json = ?, updated_at = ?
2771
+ WHERE tenant_id = ? AND workspace_id = ? AND client_id = ? AND operation_id = ?
2772
+ AND status = 'pending'
2773
+ `).run(canonicalIntentJson, now, ...ids);
2774
+ if (Number(updated.changes || 0) !== 1) {
2775
+ throw new Error('Workspace sync canonical intent changed before it was persisted.');
2776
+ }
2777
+ }
2778
+ return { enqueued: true, mutationResult };
2765
2779
  }
2766
2780
  const existing = this.db.prepare(`
2767
2781
  SELECT contract_version, dispatch_class, domain, entity_id, mutation_kind,
@@ -3891,7 +3905,7 @@ export class WorkspaceSyncDurabilityStore {
3891
3905
  mutation_kind, owns_task_metadata, owns_task_lifecycle, owns_task_assignee,
3892
3906
  CAST(base_entity_revision AS TEXT) AS base_entity_revision,
3893
3907
  CAST(base_lifecycle_revision AS TEXT) AS base_lifecycle_revision,
3894
- payload_json, payload_fingerprint, status, attempt_count,
3908
+ payload_json, payload_fingerprint, canonical_intent_json, status, attempt_count,
3895
3909
  initial_not_found_attempt_count,
3896
3910
  CAST(enqueue_sequence AS TEXT) AS enqueue_sequence,
3897
3911
  rebase_before_first_attempt,
@@ -4664,6 +4678,9 @@ export class WorkspaceSyncDurabilityStore {
4664
4678
  workflowDependencyCoalesced: true,
4665
4679
  };
4666
4680
  }
4681
+ lockWorkspaceSequenceForMutation(input) {
4682
+ this.allocateSequenceRange(requireText(input.tenantId, 'Tenant id'), requireText(input.workspaceId, 'Workspace id'), requireIsoTimestamp(input.now, 'Lock time'), 0);
4683
+ }
4667
4684
  allocateSequenceRange(tenantId, workspaceId, now, count) {
4668
4685
  const row = this.db.prepare(`
4669
4686
  INSERT INTO workspace_sync_sequences (tenant_id, workspace_id, last_sequence, updated_at)
@@ -4719,6 +4736,9 @@ export class WorkspaceSyncDurabilityStore {
4719
4736
  baseLifecycleRevision: normalizeSequence(row.base_lifecycle_revision),
4720
4737
  payloadJson: requireText(row.payload_json, 'Payload'),
4721
4738
  payloadFingerprint: requireText(row.payload_fingerprint, 'Payload fingerprint'),
4739
+ canonicalIntentJson: row.canonical_intent_json == null
4740
+ ? null
4741
+ : requireText(row.canonical_intent_json, 'Canonical intent'),
4722
4742
  status: requireText(row.status, 'Outbox status'),
4723
4743
  attemptCount: Number(row.attempt_count || 0),
4724
4744
  initialNotFoundAttemptCount: Number(row.initial_not_found_attempt_count || 0),
@@ -2,15 +2,15 @@ import type { AttachmentItem, TaskEvent, TaskEventDetails, TaskItem } from '../.
2
2
  import { serializeTaskRootForSync } from './workspaceSyncV3TaskRootProjection.js';
3
3
  import { serializeTaskActivityForSync, type WorkspaceSyncActivityRevision } from './taskActivityJournal.js';
4
4
  import { WorkspaceSyncDurabilityStore } from './syncDurabilityStore.js';
5
- import { type WorkspaceSyncV3TaskAttachmentLink, type WorkspaceSyncV3TaskAttachmentLinksPayload } from './workspaceSyncV3NestedTaskCodecs.js';
5
+ import { type WorkspaceSyncV3TaskAttachmentLink, type WorkspaceSyncV3TaskAttachmentLinksV2Payload } from './workspaceSyncV3NestedTaskCodecs.js';
6
6
  export declare class DurableTaskAttachmentLinksMutationError extends Error {
7
- readonly code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_ATTACHMENT_LINKS_INVALID' | 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_ATTACHMENT_LINKS_DEPENDENCY_PENDING' | 'TASK_ATTACHMENT_ASSET_NOT_READY';
7
+ readonly code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_ATTACHMENT_LINKS_INVALID' | 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_ATTACHMENT_LINKS_DEPENDENCY_PENDING' | 'TASK_ATTACHMENT_ASSET_NOT_READY' | 'TASK_DETAIL_REVISION_CONFLICT';
8
8
  readonly statusCode: number;
9
9
  readonly details?: Record<string, unknown> | undefined;
10
10
  readonly retryable?: boolean | undefined;
11
11
  readonly retryAfterAt?: string | undefined;
12
12
  readonly retryHint?: string | undefined;
13
- constructor(message: string, code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_ATTACHMENT_LINKS_INVALID' | 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_ATTACHMENT_LINKS_DEPENDENCY_PENDING' | 'TASK_ATTACHMENT_ASSET_NOT_READY', statusCode: number, details?: Record<string, unknown> | undefined, retryable?: boolean | undefined, retryAfterAt?: string | undefined, retryHint?: string | undefined);
13
+ constructor(message: string, code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_ATTACHMENT_LINKS_INVALID' | 'TASK_ATTACHMENT_LINKS_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_ATTACHMENT_LINKS_DEPENDENCY_PENDING' | 'TASK_ATTACHMENT_ASSET_NOT_READY' | 'TASK_DETAIL_REVISION_CONFLICT', statusCode: number, details?: Record<string, unknown> | undefined, retryable?: boolean | undefined, retryAfterAt?: string | undefined, retryHint?: string | undefined);
14
14
  }
15
15
  export type DurableTaskAttachmentLinksResponse = {
16
16
  success: true;
@@ -37,7 +37,7 @@ export type DurableTaskAttachmentLinksResponse = {
37
37
  sequence: string;
38
38
  entityRevision: string;
39
39
  lifecycleRevision: string;
40
- payload: WorkspaceSyncV3TaskAttachmentLinksPayload;
40
+ payload: WorkspaceSyncV3TaskAttachmentLinksV2Payload;
41
41
  };
42
42
  };
43
43
  activity: ReturnType<typeof serializeTaskActivityForSync>;
@@ -108,5 +108,7 @@ export declare function runDurableTaskAttachmentLinksReplace(input: {
108
108
  actorRef: string;
109
109
  resolveActorRef?: () => string;
110
110
  originActorRef?: string;
111
+ expectedDetailRevision?: string;
112
+ assertPrecondition?: () => void;
111
113
  onPostCommitError?: (error: unknown) => void;
112
114
  }): DurableTaskAttachmentLinksMutationResult;
@@ -3,7 +3,7 @@ import { serializeTaskRootForSync } from './workspaceSyncV3TaskRootProjection.js
3
3
  import { buildTaskAttachmentLinksActivityEventId } from './taskAttachmentLinksActivityIdentity.js';
4
4
  import { asWorkspaceSyncActivityRevision, buildTaskActivityJournalEntry, canonicalizeEntityActivityDetailsForSync, serializeTaskActivityForSync, } from './taskActivityJournal.js';
5
5
  import { fingerprintWorkspaceSyncPayload, runReservedWorkspaceSyncV3Operation, WORKSPACE_SYNC_CONTRACT_VERSION, } from './syncDurabilityStore.js';
6
- import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, } from './workspaceSyncV3NestedTaskCodecs.js';
6
+ import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload, } from './workspaceSyncV3NestedTaskCodecs.js';
7
7
  import { resolveDeferredMutationActorRef } from './workspaceSyncV3OriginActor.js';
8
8
  export class DurableTaskAttachmentLinksMutationError extends Error {
9
9
  code;
@@ -61,6 +61,9 @@ export function runDurableTaskAttachmentLinksReplace(input) {
61
61
  ? { cardCoverAssetId: input.cardCoverAssetId }
62
62
  : {}),
63
63
  ...(input.originActorRef ? { originActorRef: input.originActorRef } : {}),
64
+ ...(input.expectedDetailRevision
65
+ ? { expectedDetailRevision: input.expectedDetailRevision }
66
+ : {}),
64
67
  },
65
68
  });
66
69
  const activityEventId = buildTaskAttachmentLinksActivityEventId({
@@ -74,6 +77,16 @@ export function runDurableTaskAttachmentLinksReplace(input) {
74
77
  throw new Error('Durable task attachment-link activity was not persisted.');
75
78
  return activity;
76
79
  };
80
+ const readAttachmentPayload = () => {
81
+ const task = input.core.getTask(input.taskId, input.workspaceId);
82
+ if (!task)
83
+ throw new DurableTaskAttachmentLinksMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
84
+ return serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload({
85
+ taskId: input.taskId,
86
+ links: input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.workspaceId),
87
+ cardCoverAssetId: task.cardCoverAssetId ?? null,
88
+ });
89
+ };
77
90
  const result = runReservedWorkspaceSyncV3Operation({
78
91
  store: input.store, identity, requestFingerprint,
79
92
  now: input.now, leaseExpiresAt: input.leaseExpiresAt, leaseOwner: input.leaseOwner,
@@ -101,16 +114,11 @@ export function runDurableTaskAttachmentLinksReplace(input) {
101
114
  }, {
102
115
  domain: 'task-attachment-links', entityId: input.taskId, mutationKind: 'replace', revisionKind: 'metadata',
103
116
  baseEntityRevision: input.baseEntityRevision, baseLifecycleRevision: input.baseLifecycleRevision,
104
- buildCanonicalFingerprint: () => fingerprintWorkspaceSyncPayload(serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
105
- taskId: input.taskId,
106
- links: input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.workspaceId),
107
- })),
108
- buildAuthoritativePayload: () => serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
109
- taskId: input.taskId,
110
- links: input.core.listTaskAttachmentLinksForDurableSync(input.taskId, input.workspaceId),
111
- }),
117
+ buildCanonicalFingerprint: () => fingerprintWorkspaceSyncPayload(readAttachmentPayload()),
118
+ buildAuthoritativePayload: readAttachmentPayload,
112
119
  }, buildTaskActivityJournalEntry({ eventId: activityEventId, readEvent: readActivity })],
113
120
  applyMutation: () => {
121
+ input.assertPrecondition?.();
114
122
  const existing = input.core.getTask(input.taskId, input.workspaceId);
115
123
  if (!existing)
116
124
  throw new DurableTaskAttachmentLinksMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
@@ -181,9 +189,10 @@ export function runDurableTaskAttachmentLinksReplace(input) {
181
189
  sequence: attachmentLinksRevision.sequence,
182
190
  entityRevision: attachmentLinksRevision.entityRevision,
183
191
  lifecycleRevision: attachmentLinksRevision.lifecycleRevision,
184
- payload: serializeWorkspaceSyncV3TaskAttachmentLinksPayload({
192
+ payload: serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload({
185
193
  taskId: task.id,
186
194
  links: input.core.listTaskAttachmentLinksForDurableSync(task.id, input.workspaceId),
195
+ cardCoverAssetId: task.cardCoverAssetId ?? null,
187
196
  }),
188
197
  },
189
198
  },
@@ -4,13 +4,13 @@ import { serializeTaskActivityForSync, type WorkspaceSyncActivityRevision } from
4
4
  import { WorkspaceSyncDurabilityStore } from './syncDurabilityStore.js';
5
5
  import { type WorkspaceSyncV3TaskChecklistPayload } from './workspaceSyncV3NestedTaskCodecs.js';
6
6
  export declare class DurableTaskChecklistMutationError extends Error {
7
- readonly code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_CHECKLIST_INVALID' | 'TASK_CHECKLIST_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_CHECKLIST_DEPENDENCY_PENDING';
7
+ readonly code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_CHECKLIST_INVALID' | 'TASK_CHECKLIST_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_CHECKLIST_DEPENDENCY_PENDING' | 'TASK_DETAIL_REVISION_CONFLICT';
8
8
  readonly statusCode: number;
9
9
  readonly details?: Record<string, unknown> | undefined;
10
10
  readonly retryable?: boolean | undefined;
11
11
  readonly retryAfterAt?: string | undefined;
12
12
  readonly retryHint?: string | undefined;
13
- constructor(message: string, code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_CHECKLIST_INVALID' | 'TASK_CHECKLIST_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_CHECKLIST_DEPENDENCY_PENDING', statusCode: number, details?: Record<string, unknown> | undefined, retryable?: boolean | undefined, retryAfterAt?: string | undefined, retryHint?: string | undefined);
13
+ constructor(message: string, code: 'TASK_NOT_FOUND' | 'TASK_ARCHIVED_READ_ONLY' | 'TASK_CHECKLIST_INVALID' | 'TASK_CHECKLIST_IDENTITY_CONFLICT' | 'WORKSPACE_SYNC_OPERATION_ID_REQUIRED' | 'TASK_CHECKLIST_DEPENDENCY_PENDING' | 'TASK_DETAIL_REVISION_CONFLICT', statusCode: number, details?: Record<string, unknown> | undefined, retryable?: boolean | undefined, retryAfterAt?: string | undefined, retryHint?: string | undefined);
14
14
  }
15
15
  export type DurableTaskChecklistResponse = {
16
16
  success: true;
@@ -88,5 +88,7 @@ export declare function runDurableTaskChecklistReplace(input: {
88
88
  actorRef: string;
89
89
  resolveActorRef?: () => string;
90
90
  originActorRef?: string;
91
+ expectedDetailRevision?: string;
92
+ assertPrecondition?: () => void;
91
93
  onPostCommitError?: (error: unknown) => void;
92
94
  }): DurableTaskChecklistMutationResult;
@@ -58,6 +58,9 @@ export function runDurableTaskChecklistReplace(input) {
58
58
  baseLifecycleRevision: String(input.baseLifecycleRevision),
59
59
  items,
60
60
  ...(input.originActorRef ? { originActorRef: input.originActorRef } : {}),
61
+ ...(input.expectedDetailRevision
62
+ ? { expectedDetailRevision: input.expectedDetailRevision }
63
+ : {}),
61
64
  },
62
65
  });
63
66
  const activityEventId = buildTaskChecklistActivityEventId({
@@ -100,6 +103,7 @@ export function runDurableTaskChecklistReplace(input) {
100
103
  }),
101
104
  }, buildTaskActivityJournalEntry({ eventId: activityEventId, readEvent: readActivity })],
102
105
  applyMutation: () => {
106
+ input.assertPrecondition?.();
103
107
  const existing = input.core.getTask(input.taskId, input.workspaceId);
104
108
  if (!existing)
105
109
  throw new DurableTaskChecklistMutationError('Task not found.', 'TASK_NOT_FOUND', 404);
@@ -2,7 +2,7 @@ import { type DurableTaskCreateMutationCore } from '../../core/TaskDurableCreate
2
2
  import type { EntityEvent, TaskEvent, TaskItem } from '../../core/Taskforce.js';
3
3
  import { serializeTaskForSync } from '../taskSyncPayload.js';
4
4
  import type { WorkspaceSyncTaskEventSnapshot } from '../workspaceSyncModel.js';
5
- import { serializeWorkspaceSyncV3TaskAttachmentLinksPayload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, type WorkspaceSyncV3TaskAttachmentLink } from './workspaceSyncV3NestedTaskCodecs.js';
5
+ import { serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload, serializeWorkspaceSyncV3TaskChecklistPayload, serializeWorkspaceSyncV3TaskCommentsPayload, type WorkspaceSyncV3TaskAttachmentLink } from './workspaceSyncV3NestedTaskCodecs.js';
6
6
  import { type WorkspaceSyncActivityRevision } from './taskActivityJournal.js';
7
7
  import { WorkspaceSyncDurabilityStore } from './syncDurabilityStore.js';
8
8
  export interface DurableTaskCreateServiceCore extends DurableTaskCreateMutationCore {
@@ -64,7 +64,7 @@ export type DurableTaskCreateResponse = {
64
64
  sequence: string;
65
65
  entityRevision: string;
66
66
  lifecycleRevision: string;
67
- payload: ReturnType<typeof serializeWorkspaceSyncV3TaskAttachmentLinksPayload>;
67
+ payload: ReturnType<typeof serializeWorkspaceSyncV3TaskAttachmentLinksV2Payload>;
68
68
  };
69
69
  activity: WorkspaceSyncTaskEventSnapshot;
70
70
  activityRevision: WorkspaceSyncActivityRevision;