@taskforcehq/taskforce 0.3.327 → 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 (360) hide show
  1. package/dist/Taskforce.module.css +257 -87
  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/activity/EntityActivityTimeline.js +15 -5
  8. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  9. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  10. package/dist/components/context/ContextAttachmentManager.js +116 -20
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  12. package/dist/components/features/TaskSettings.d.ts +12 -1
  13. package/dist/components/features/TaskSettings.js +147 -40
  14. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  15. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  16. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  17. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  18. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  19. package/dist/components/features/planning/PlanningModule.js +28 -14
  20. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  21. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  22. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  23. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  24. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  25. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  26. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  27. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  28. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  29. package/dist/components/task/TaskActionHeader.js +2 -2
  30. package/dist/components/task/TaskCard.d.ts +5 -0
  31. package/dist/components/task/TaskCard.js +101 -13
  32. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  33. package/dist/components/task/TaskContextUpload.js +2 -2
  34. package/dist/components/task/TaskForm.d.ts +4 -0
  35. package/dist/components/task/TaskForm.js +32 -7
  36. package/dist/components/task/TaskImageReviews.js +28 -19
  37. package/dist/components/task/TaskKanban.d.ts +6 -1
  38. package/dist/components/task/TaskKanban.js +152 -6
  39. package/dist/components/task/TaskList.d.ts +5 -0
  40. package/dist/components/task/TaskList.js +3 -3
  41. package/dist/components/task/TaskStatusActions.js +11 -2
  42. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  43. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  44. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  45. package/dist/components/ui/PasswordInput.d.ts +6 -0
  46. package/dist/components/ui/PasswordInput.js +11 -0
  47. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  48. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  49. package/dist/components/ui/TopNoticeLayer.js +1 -1
  50. package/dist/components/views/AppShellHeader.js +32 -11
  51. package/dist/components/views/PlanComparisonPage.js +12 -0
  52. package/dist/components/views/StandaloneLayout.js +68 -39
  53. package/dist/components/views/WidgetView.js +10 -7
  54. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  55. package/dist/components/views/panels/PlanningDrawer.js +51 -54
  56. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  57. package/dist/config/envSchema.js +13 -0
  58. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  59. package/dist/config/localServiceCloudTarget.js +76 -0
  60. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  61. package/dist/config/localServiceRuntimeContract.js +135 -0
  62. package/dist/core/AiProfileService.d.ts +1 -0
  63. package/dist/core/AiProfileService.js +48 -9
  64. package/dist/core/AttachmentLinkService.js +7 -4
  65. package/dist/core/DeletedTaskLifecycleService.d.ts +5 -0
  66. package/dist/core/DeletedTaskLifecycleService.js +55 -3
  67. package/dist/core/McpTokenService.d.ts +4 -0
  68. package/dist/core/McpTokenService.js +52 -0
  69. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +8 -2
  70. package/dist/core/TaskAttachmentLinksDurableMutationService.js +51 -18
  71. package/dist/core/Taskforce.d.ts +124 -2
  72. package/dist/core/Taskforce.js +1175 -87
  73. package/dist/core/types.d.ts +8 -1
  74. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  75. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  76. package/dist/documentReviews/service.d.ts +4 -0
  77. package/dist/documentReviews/service.js +141 -46
  78. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  79. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  80. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  81. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  82. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  83. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  84. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  85. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  86. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  87. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  88. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  89. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  90. package/dist/hooks/tasks/useTaskFormActions.d.ts +9 -0
  91. package/dist/hooks/tasks/useTaskFormActions.js +96 -17
  92. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  93. package/dist/hooks/ui/useSettingsModel.js +13 -2
  94. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  95. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  96. package/dist/hooks/useFilterSort.d.ts +7 -3
  97. package/dist/hooks/useFilterSort.js +15 -5
  98. package/dist/hooks/useTaskData.d.ts +10 -1
  99. package/dist/hooks/useTaskData.js +216 -7
  100. package/dist/hooks/useTaskMutations.d.ts +10 -2
  101. package/dist/hooks/useTaskMutations.js +223 -43
  102. package/dist/hooks/useTaskforce.d.ts +94 -57
  103. package/dist/hooks/useTaskforce.js +428 -16
  104. package/dist/hooks/useUiNotice.d.ts +4 -0
  105. package/dist/hooks/useUiNotice.js +4 -0
  106. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  109. package/dist/mcp/collaborationRegistrar.js +15 -13
  110. package/dist/mcp/documentAssetRegistrar.js +60 -44
  111. package/dist/mcp/documentHelpers.d.ts +1 -0
  112. package/dist/mcp/documentHelpers.js +7 -3
  113. package/dist/mcp/localServiceProxy.js +4 -1
  114. package/dist/mcp/runtime.d.ts +16 -0
  115. package/dist/mcp/runtime.js +735 -210
  116. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  117. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  118. package/dist/mcp/taskPlanningRegistrar.js +78 -67
  119. package/dist/mcp/toolCallCompatibility.js +38 -1
  120. package/dist/mcp/toolCatalog.d.ts +1 -0
  121. package/dist/mcp/toolRegistry.js +2 -2
  122. package/dist/migrations/taskSchemaMigrations.js +47 -0
  123. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  124. package/dist/runtime/appGateDefinitions.js +6 -6
  125. package/dist/runtime/appGates.d.ts +12 -13
  126. package/dist/runtime/appGates.js +29 -13
  127. package/dist/runtime/nodeAppGates.d.ts +8 -0
  128. package/dist/runtime/nodeAppGates.js +27 -0
  129. package/dist/server/appAccess.d.ts +12 -0
  130. package/dist/server/appAccess.js +21 -0
  131. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  132. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  133. package/dist/server/cloudCollectionRead.d.ts +25 -0
  134. package/dist/server/cloudCollectionRead.js +120 -0
  135. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  136. package/dist/server/cloudCollectionReadPool.js +140 -0
  137. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  138. package/dist/server/cloudCollectionReadWorker.js +36 -0
  139. package/dist/server/index.d.ts +6 -11
  140. package/dist/server/index.js +67 -42
  141. package/dist/server/localServiceLease.d.ts +6 -0
  142. package/dist/server/localServiceLease.js +17 -1
  143. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  144. package/dist/server/rateLimit.d.ts +1 -0
  145. package/dist/server/rateLimit.js +4 -0
  146. package/dist/server/realtimeSyncWs.js +36 -12
  147. package/dist/server/routes/admin.js +1 -0
  148. package/dist/server/routes/agentRoles.js +3 -2
  149. package/dist/server/routes/agentSkills.js +3 -2
  150. package/dist/server/routes/agents.js +118 -16
  151. package/dist/server/routes/annotatedAttachments.js +3 -2
  152. package/dist/server/routes/auth.js +3 -1
  153. package/dist/server/routes/documents.d.ts +5 -2
  154. package/dist/server/routes/documents.js +160 -13
  155. package/dist/server/routes/durableTaskHttpRouters.d.ts +11 -2
  156. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  157. package/dist/server/routes/localService.js +4 -0
  158. package/dist/server/routes/primitives.d.ts +4 -0
  159. package/dist/server/routes/shared.d.ts +24 -0
  160. package/dist/server/routes/shared.js +114 -18
  161. package/dist/server/routes/sync.js +22 -0
  162. package/dist/server/routes/syncAuxRoutes.js +5 -2
  163. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  164. package/dist/server/routes/syncV3FeedRoutes.js +22 -18
  165. package/dist/server/routes/syncV3LocalCaptureRoutes.js +48 -4
  166. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  167. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  168. package/dist/server/routes/syncV3OperationRoutes.js +27 -2
  169. package/dist/server/routes/tasks.js +299 -97
  170. package/dist/server/routes/workspaces.js +43 -1
  171. package/dist/server/routes.js +35 -7
  172. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  173. package/dist/server/workspaceDeletionAdmission.js +50 -0
  174. package/dist/service/localServiceCli.js +52 -25
  175. package/dist/service/localServiceClientLifecycle.js +93 -6
  176. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  177. package/dist/services/codexAppServerModelGateway.js +438 -0
  178. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  179. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  180. package/dist/shared/planningIdentity.d.ts +20 -0
  181. package/dist/shared/planningIdentity.js +79 -0
  182. package/dist/shared/taskforceAgentDefinition.js +7 -1
  183. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  184. package/dist/shared/taskforceAgentModels.js +30 -1
  185. package/dist/storage/postgresAdapter.d.ts +20 -3
  186. package/dist/storage/postgresAdapter.js +110 -14
  187. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  188. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  189. package/dist/storage/postgresWorker.js +13 -4
  190. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  191. package/dist/storage/postgresWorkerProtocol.js +30 -0
  192. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  193. package/dist/storage/workspaceAssetStore.js +70 -0
  194. package/dist/sync/cloudSyncApi.d.ts +4 -1
  195. package/dist/sync/cloudSyncApi.js +7 -2
  196. package/dist/sync/contentSyncState.d.ts +1 -1
  197. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +17 -0
  198. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +77 -23
  199. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  200. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  201. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  202. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  203. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  204. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +16 -0
  205. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  206. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  207. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +46 -4
  208. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  209. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +51 -11
  210. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  211. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  212. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  213. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  214. package/dist/sync/syncApplyHandlers.js +29 -0
  215. package/dist/sync/syncService.d.ts +2 -0
  216. package/dist/sync/syncService.js +13 -1
  217. package/dist/sync/taskSyncPayload.d.ts +1 -0
  218. package/dist/sync/taskSyncPayload.js +6 -0
  219. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  220. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +3 -0
  221. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +31 -3
  222. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  223. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  224. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  225. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +47 -12
  226. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  227. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  228. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  229. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  230. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  231. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +3 -0
  232. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +62 -10
  233. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  234. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  235. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  236. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  237. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  238. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  239. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  240. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  241. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  242. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  243. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  244. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  245. package/dist/sync/v3/syncDurabilityStore.d.ts +15 -0
  246. package/dist/sync/v3/syncDurabilityStore.js +201 -43
  247. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +7 -4
  248. package/dist/sync/v3/taskAttachmentLinksMutationService.js +37 -12
  249. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  250. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  251. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  252. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  253. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  254. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  255. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  256. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  257. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  258. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  259. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  260. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  261. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  262. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  263. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +14 -12
  264. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  265. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  266. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  267. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  268. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +10 -1
  269. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +15 -4
  270. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +66 -13
  271. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +11 -0
  272. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +57 -22
  273. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +10 -4
  274. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +124 -51
  275. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  276. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +120 -28
  277. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  278. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  279. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  280. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  281. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  282. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  283. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  284. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  285. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  286. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  287. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  288. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  289. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  290. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  291. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +3 -0
  292. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  293. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  294. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  295. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  296. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  297. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  298. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  299. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  300. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  301. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  302. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  303. package/dist/sync/workspacePullFeed.d.ts +6 -1
  304. package/dist/sync/workspacePullFeed.js +174 -162
  305. package/dist/sync/workspaceRepair.d.ts +3 -0
  306. package/dist/sync/workspaceRepair.js +67 -6
  307. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  308. package/dist/sync/workspaceSyncModel.js +27 -2
  309. package/dist/types.d.ts +55 -1
  310. package/dist/types.js +6 -0
  311. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  312. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  313. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  314. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  315. package/dist/ui/assets/AssetTraySortControl-C8Rztyal.js +1 -0
  316. package/dist/ui/assets/ContextAttachmentManager-Cj3c7X9y.js +3 -0
  317. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-B5uvzems.js} +4 -4
  318. package/dist/ui/assets/EntityActivityTimeline-BVFvCB5R.js +1 -0
  319. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  320. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  321. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  322. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  323. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  324. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  325. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  326. package/dist/ui/assets/WorkflowManagerModule-DEr5di_r.js +1 -0
  327. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  328. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  329. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-D9Lpw-j4.js} +1 -1
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/constants.d.ts +2 -2
  332. package/dist/utils/constants.js +1 -1
  333. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  334. package/dist/utils/contextAttachmentUpload.js +7 -0
  335. package/dist/utils/contextFiles.d.ts +1 -1
  336. package/dist/utils/contextFiles.js +4 -4
  337. package/dist/utils/taskActivity.d.ts +3 -2
  338. package/dist/utils/taskActivity.js +19 -2
  339. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  340. package/dist/utils/taskEditorReconciliation.js +217 -0
  341. package/dist/utils/taskNormalization.d.ts +1 -0
  342. package/dist/utils/taskNormalization.js +39 -0
  343. package/dist/utils/taskSearch.js +2 -2
  344. package/dist/utils/uiNotice.d.ts +4 -0
  345. package/package.json +21 -4
  346. package/dist/ui/assets/AiProfilesModule-DPxQeF9N.js +0 -1
  347. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  348. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DHNYaNg6.js +0 -3
  349. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  350. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  351. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  352. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  353. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  354. package/dist/ui/assets/PlansPage-BVH_NfgT.js +0 -1
  355. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  356. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  357. package/dist/ui/assets/TaskSettings-xVsxSa0g.js +0 -12
  358. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  359. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  360. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -10,8 +10,9 @@ export declare function createTaskHttpDurableRouters(input: {
10
10
  actorRef: string;
11
11
  root: {
12
12
  enabled: boolean;
13
+ hasCommittedOperation(): boolean;
13
14
  create(candidate: unknown): import("../../core/Taskforce.js").TaskItem;
14
- update(taskIdInput: string, candidate: unknown, saveSource: "manual" | "autosave", activityAction?: "task-updated" | "task-schedule-changed"): import("../../core/Taskforce.js").TaskItem;
15
+ update(taskIdInput: string, candidate: unknown, saveSource: "manual" | "autosave", activityAction?: "task-updated" | "task-schedule-changed", expectedDetailRevision?: string): import("../../core/Taskforce.js").TaskItem;
15
16
  runBatch<T>(routeInput: string, request: unknown, apply: (context: {
16
17
  runChild<R>(mutation: () => R): R;
17
18
  deferPostCommitNotification(notify: () => void): void;
@@ -41,6 +42,7 @@ export declare function createTaskHttpDurableRouters(input: {
41
42
  replace(args: {
42
43
  taskId: string;
43
44
  items: readonly Partial<import("../../core/Taskforce.js").ChecklistItem>[];
45
+ expectedDetailRevision?: string;
44
46
  }): {
45
47
  state: "pending" | "committed";
46
48
  route: "local-outbox" | "cloud-acceptance";
@@ -53,6 +55,8 @@ export declare function createTaskHttpDurableRouters(input: {
53
55
  replace(args: {
54
56
  taskId: string;
55
57
  links: readonly import("../../sync/v3/workspaceSyncV3NestedTaskCodecs.js").WorkspaceSyncV3TaskAttachmentLink[];
58
+ cardCoverAssetId?: string | null;
59
+ expectedDetailRevision?: string;
56
60
  }): {
57
61
  state: "pending" | "committed";
58
62
  route: "local-outbox" | "cloud-acceptance";
@@ -65,6 +69,7 @@ export declare function createTaskHttpDurableRouters(input: {
65
69
  replace(args: {
66
70
  taskId: string;
67
71
  items: readonly Partial<import("../../core/Taskforce.js").ChecklistItem>[];
72
+ expectedDetailRevision?: string;
68
73
  }): {
69
74
  state: "pending" | "committed";
70
75
  route: "local-outbox" | "cloud-acceptance";
@@ -77,6 +82,8 @@ export declare function createTaskHttpDurableRouters(input: {
77
82
  replace(args: {
78
83
  taskId: string;
79
84
  links: readonly import("../../sync/v3/workspaceSyncV3NestedTaskCodecs.js").WorkspaceSyncV3TaskAttachmentLink[];
85
+ cardCoverAssetId?: string | null;
86
+ expectedDetailRevision?: string;
80
87
  }): {
81
88
  state: "pending" | "committed";
82
89
  route: "local-outbox" | "cloud-acceptance";
@@ -132,6 +139,7 @@ export declare function createTaskHttpDurableRouters(input: {
132
139
  }>, options?: {
133
140
  saveSource?: "manual" | "autosave";
134
141
  atomicItemFailures?: boolean;
142
+ expectedDetailRevision?: string;
135
143
  }): {
136
144
  preview: import("../../core/Taskforce.js").BulkUpdateFieldsPreview;
137
145
  results: import("../../core/Taskforce.js").TaskItem[];
@@ -147,8 +155,9 @@ export declare function createTaskHttpDurableRouters(input: {
147
155
  };
148
156
  createRootRouter: (operationId?: string, deferPostCommitNotification?: (notify: () => void) => void) => {
149
157
  enabled: boolean;
158
+ hasCommittedOperation(): boolean;
150
159
  create(candidate: unknown): import("../../core/Taskforce.js").TaskItem;
151
- update(taskIdInput: string, candidate: unknown, saveSource: "manual" | "autosave", activityAction?: "task-updated" | "task-schedule-changed"): import("../../core/Taskforce.js").TaskItem;
160
+ update(taskIdInput: string, candidate: unknown, saveSource: "manual" | "autosave", activityAction?: "task-updated" | "task-schedule-changed", expectedDetailRevision?: string): import("../../core/Taskforce.js").TaskItem;
152
161
  runBatch<T>(routeInput: string, request: unknown, apply: (context: {
153
162
  runChild<R>(mutation: () => R): R;
154
163
  deferPostCommitNotification(notify: () => void): void;
@@ -4,12 +4,12 @@ import { createDurableTaskHttpRootMutationRouter } from '../../sync/v3/durableTa
4
4
  import { createDurableTaskCommentMutationRouter } from '../../sync/v3/durableTaskCommentMutationRouter.js';
5
5
  import { createDurableTaskChecklistMutationRouter } from '../../sync/v3/durableTaskChecklistMutationRouter.js';
6
6
  import { createDurableTaskAttachmentLinksMutationRouter } from '../../sync/v3/durableTaskAttachmentLinksMutationRouter.js';
7
- import { requestHeader, requestRuntimeMode, requestUserId } from './shared.js';
7
+ import { requestHeader, requestHumanActorRef, requestRuntimeMode } from './shared.js';
8
8
  export function createTaskHttpDurableRouters(input) {
9
9
  const { core, context, req, workspaceId } = input;
10
10
  const runtimeMode = context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : requestRuntimeMode(req);
11
11
  const parentOperationId = requestHeader(req, 'x-taskforce-operation-id', '').trim();
12
- const actorRef = runtimeMode === 'cloud' ? (String(requestUserId(req) || '').trim() || 'user') : 'user';
12
+ const actorRef = requestHumanActorRef(req);
13
13
  const createRootRouter = (operationId = parentOperationId, deferPostCommitNotification) => createDurableTaskHttpRootMutationRouter({
14
14
  core,
15
15
  runtimeMode,
@@ -185,6 +185,8 @@ export function registerLocalServiceRoutes(deps) {
185
185
  endpoint: context.endpoint?.() || null,
186
186
  projectIdentity: context.projectIdentity,
187
187
  dataIdentity: context.ownershipKey,
188
+ cloudTarget: context.cloudTarget,
189
+ runtimeContract: context.runtimeContract,
188
190
  transports: ['streamable-http'],
189
191
  health: {
190
192
  lifecycle: context.sessions.state(),
@@ -279,6 +281,8 @@ export function registerLocalServiceRoutes(deps) {
279
281
  generation: context.generation,
280
282
  protocolVersion: context.protocolVersion,
281
283
  ownerPid: context.ownerPid,
284
+ cloudTarget: context.cloudTarget,
285
+ runtimeContract: context.runtimeContract,
282
286
  sessions: context.sessions.list(),
283
287
  ...(context.diagnostics?.() || {}),
284
288
  });
@@ -4,6 +4,8 @@ import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js
4
4
  import type { LocalSyncCoordinatorRouteServices } from '../../sync/coordinator/localSyncCoordinatorRouteServices.js';
5
5
  import type { WorkspaceSyncServerDataServices } from '../../sync/engine/workspaceSyncServerDataServices.js';
6
6
  import type { LocalServiceSessionManager } from '../localServiceSessions.js';
7
+ import type { LocalServiceCloudTarget } from '../../config/localServiceCloudTarget.js';
8
+ import type { LocalServiceRuntimeContract } from '../../config/localServiceRuntimeContract.js';
7
9
  export interface LocalServiceRouteContext {
8
10
  generation: string;
9
11
  protocolVersion: string;
@@ -11,6 +13,8 @@ export interface LocalServiceRouteContext {
11
13
  ownerPid: number;
12
14
  bootstrapCapability: string;
13
15
  ownershipKey: string;
16
+ cloudTarget: LocalServiceCloudTarget;
17
+ runtimeContract: LocalServiceRuntimeContract;
14
18
  projectIdentity: string;
15
19
  operationClientId: string;
16
20
  sessions: LocalServiceSessionManager;
@@ -11,6 +11,7 @@ import type { McpAccessTokenAuthResult, McpOAuthAccessTokenAuthResult, McpAccess
11
11
  import { type RouteContext } from './primitives.js';
12
12
  import { type AiProfileBootstrapConfig } from '../../mcp/aiProfileBootstrap.js';
13
13
  import type { AiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
14
+ import { type RequestPerformanceDiagnostics } from '../../storage/postgresRequestDiagnostics.js';
14
15
  export declare function resolveErrorStatusCode(error: unknown, fallback?: number): number;
15
16
  export declare function buildCloudMcpProfileConnectionId(input: {
16
17
  oauth?: McpOAuthAccessTokenAuthResult | null;
@@ -19,6 +20,7 @@ export declare function buildCloudMcpProfileConnectionId(input: {
19
20
  }): string | null;
20
21
  export declare const BOOTSTRAP_ROUTE_SLOW_THRESHOLD_MS = 1500;
21
22
  export declare const BOOTSTRAP_ROUTE_SEVERE_THRESHOLD_MS = 5000;
23
+ export declare const TASKFORCE_PERFORMANCE_TRACE_HEADER = "x-taskforce-performance-trace";
22
24
  export type ServerTimingMetric = {
23
25
  name: string;
24
26
  durationMs: number;
@@ -31,6 +33,17 @@ export declare function serializeJsonWithTiming(payload: unknown, startedAt?: nu
31
33
  byteLength: number;
32
34
  durationMs: number;
33
35
  };
36
+ export declare function runWithPerformanceTrace<T>(req: IncomingMessage, callback: () => T): T;
37
+ export declare function currentPerformanceTraceDiagnostics(): RequestPerformanceDiagnostics | null;
38
+ export declare function performanceTraceServerTimingMetrics(diagnostics: RequestPerformanceDiagnostics | null): ServerTimingMetric[];
39
+ export declare function setPerformanceTraceHeaders(res: ServerResponse, diagnostics: RequestPerformanceDiagnostics | null, responseBytes: number): void;
40
+ export declare function respondWithPerformanceTraceJson(input: {
41
+ req: IncomingMessage;
42
+ res: ServerResponse;
43
+ route: string;
44
+ workspaceId: string;
45
+ load: () => unknown;
46
+ }): void;
34
47
  export declare function requestBootstrapTraceId(req: IncomingMessage): string;
35
48
  export declare function logSlowBootstrapRoute(input: {
36
49
  route: string;
@@ -47,6 +60,7 @@ export declare function buildCloudMcpRuntimeAuth(input: {
47
60
  authenticatedViaToken: McpAccessTokenAuthResult | null;
48
61
  authenticatedViaOAuth: McpOAuthAccessTokenAuthResult | null;
49
62
  userId: string | null;
63
+ aiProfileIdentityAsserted?: boolean;
50
64
  } & Required<AiProfileBootstrapConfig>;
51
65
  requestHeader: (req: IncomingMessage, name: string, fallback?: string) => string;
52
66
  requestId?: string | null;
@@ -57,6 +71,7 @@ export declare function buildCloudMcpRuntimeAuth(input: {
57
71
  tokenId?: string | null;
58
72
  aiProfileToken?: AiProfileBootstrapConfig['aiProfileToken'];
59
73
  aiProfileId?: string | null;
74
+ aiProfileIdentityAsserted?: boolean;
60
75
  aiProfileName?: AiProfileBootstrapConfig['aiProfileName'];
61
76
  aiProfileIcon?: AiProfileBootstrapConfig['aiProfileIcon'];
62
77
  aiProfileColor?: AiProfileBootstrapConfig['aiProfileColor'];
@@ -79,6 +94,7 @@ export type CloudMcpRequestAuth = {
79
94
  authenticatedViaOAuth: McpOAuthAccessTokenAuthResult | null;
80
95
  userId: string | null;
81
96
  aiProfileId?: string | null;
97
+ aiProfileIdentityAsserted?: boolean;
82
98
  } & Required<AiProfileBootstrapConfig>;
83
99
  export declare function makeAuthenticateCloudMcpRequest(core: TaskforceCore, deps: {
84
100
  resolveAuthenticatedUserId: (req: IncomingMessage) => string;
@@ -101,6 +117,14 @@ export declare function requestRuntimeMode(req: IncomingMessage): 'local' | 'clo
101
117
  export declare function requestRole(req: IncomingMessage): string;
102
118
  export declare function requestSystemRole(req: IncomingMessage): 'system_admin' | 'user';
103
119
  export declare function requestUserId(req: IncomingMessage): string;
120
+ /**
121
+ * Resolve the human actor recorded by ordinary HTTP mutations. The standalone
122
+ * server has already replaced this header with authenticated cloud identity
123
+ * when available; local signed-in clients provide the same account id through
124
+ * the request-scoped mutation header. Anonymous sessions retain the legacy
125
+ * generic actor for compatibility.
126
+ */
127
+ export declare function requestHumanActorRef(req: IncomingMessage): string;
104
128
  export declare function makeRequestWorkspaceId(core: TaskforceCore, context: RouteContext): (req: IncomingMessage) => string;
105
129
  export declare function makeResolveAuthenticatedUserId(core: TaskforceCore, context: RouteContext, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage) => string;
106
130
  export declare function makeResolveAuthenticatedMutationUserId(core: TaskforceCore, context: RouteContext, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage) => string;
@@ -17,7 +17,9 @@ import { resolveCanonicalMcpToolName } from '../../mcp/toolCallCompatibility.js'
17
17
  import { applyPrefixedRateLimitHeaders } from '../rateLimit.js';
18
18
  import { parseAiProfileBootstrapHeaders } from '../../mcp/aiProfileBootstrap.js';
19
19
  import { parseMcpClientId } from '../../mcp/clientRegistry.js';
20
+ import { resolveAppAccessDeniedResponse } from '../appAccess.js';
20
21
  import { resolveCloudApplicationBaseUrl } from '../../config/applicationBaseUrl.js';
22
+ import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../../storage/postgresRequestDiagnostics.js';
21
23
  // === SECTION: Error helpers ===
22
24
  export function resolveErrorStatusCode(error, fallback = 400) {
23
25
  if (error instanceof TaskforceRuleError)
@@ -39,6 +41,7 @@ export function buildCloudMcpProfileConnectionId(input) {
39
41
  // === SECTION: Route timing helpers ===
40
42
  export const BOOTSTRAP_ROUTE_SLOW_THRESHOLD_MS = 1500;
41
43
  export const BOOTSTRAP_ROUTE_SEVERE_THRESHOLD_MS = 5000;
44
+ export const TASKFORCE_PERFORMANCE_TRACE_HEADER = 'x-taskforce-performance-trace';
42
45
  function sanitizeServerTimingMetricName(name) {
43
46
  return String(name || 'taskforce-route')
44
47
  .trim()
@@ -68,6 +71,55 @@ export function serializeJsonWithTiming(payload, startedAt = getRouteTimingNow()
68
71
  durationMs: Math.max(0, getRouteTimingNow() - startedAt),
69
72
  };
70
73
  }
74
+ export function runWithPerformanceTrace(req, callback) {
75
+ const enabled = requestHeader(req, TASKFORCE_PERFORMANCE_TRACE_HEADER, '').trim() === '1';
76
+ return runWithRequestPerformanceDiagnostics(enabled, callback);
77
+ }
78
+ export function currentPerformanceTraceDiagnostics() {
79
+ return getRequestPerformanceDiagnostics();
80
+ }
81
+ export function performanceTraceServerTimingMetrics(diagnostics) {
82
+ return diagnostics ? [{ name: 'postgres-worker-wait', durationMs: diagnostics.postgresWorkerWaitMs }] : [];
83
+ }
84
+ export function setPerformanceTraceHeaders(res, diagnostics, responseBytes) {
85
+ if (!diagnostics)
86
+ return;
87
+ res.setHeader('X-Taskforce-Postgres-Query-Count', String(diagnostics.postgresQueryCount));
88
+ res.setHeader('X-Taskforce-Postgres-Worker-Call-Count', String(diagnostics.postgresWorkerCallCount));
89
+ res.setHeader('X-Taskforce-Response-Bytes', String(responseBytes));
90
+ }
91
+ export function respondWithPerformanceTraceJson(input) {
92
+ runWithPerformanceTrace(input.req, () => {
93
+ const startedAt = getRouteTimingNow();
94
+ const data = input.load();
95
+ const loadCompletedAt = getRouteTimingNow();
96
+ const serialized = serializeJsonWithTiming(data);
97
+ const diagnostics = getRequestPerformanceDiagnostics();
98
+ const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
99
+ setServerTimingHeader(input.res, [
100
+ { name: 'route-total', durationMs: totalDurationMs },
101
+ { name: 'route-load', durationMs: loadCompletedAt - startedAt },
102
+ { name: 'route-json', durationMs: serialized.durationMs },
103
+ ...performanceTraceServerTimingMetrics(diagnostics),
104
+ ]);
105
+ setPerformanceTraceHeaders(input.res, diagnostics, serialized.byteLength);
106
+ logSlowBootstrapRoute({
107
+ route: input.route,
108
+ workspaceId: input.workspaceId,
109
+ traceId: requestBootstrapTraceId(input.req),
110
+ durationMs: totalDurationMs,
111
+ details: diagnostics ? {
112
+ responseBytes: serialized.byteLength,
113
+ postgresQueryCount: diagnostics.postgresQueryCount,
114
+ postgresWorkerCallCount: diagnostics.postgresWorkerCallCount,
115
+ postgresWorkerWaitMs: Math.round(diagnostics.postgresWorkerWaitMs),
116
+ jsonSerializeMs: serialized.durationMs,
117
+ } : { responseBytes: serialized.byteLength },
118
+ });
119
+ input.res.writeHead(200, { 'Content-Type': 'application/json' });
120
+ input.res.end(serialized.body);
121
+ });
122
+ }
71
123
  export function requestBootstrapTraceId(req) {
72
124
  return requestHeader(req, 'x-taskforce-bootstrap-trace-id', '').trim();
73
125
  }
@@ -100,6 +152,7 @@ export function buildCloudMcpRuntimeAuth(input) {
100
152
  tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
101
153
  aiProfileToken: auth.aiProfileToken,
102
154
  aiProfileId: auth.authenticatedViaOAuth?.aiProfileId || null,
155
+ aiProfileIdentityAsserted: auth.aiProfileIdentityAsserted === true,
103
156
  aiProfileName: auth.aiProfileName,
104
157
  aiProfileIcon: auth.aiProfileIcon,
105
158
  aiProfileColor: auth.aiProfileColor,
@@ -183,6 +236,7 @@ export function makeAuthenticateCloudMcpRequest(core, deps) {
183
236
  userId: null,
184
237
  ...bootstrap,
185
238
  aiProfileId: oauthAuth?.aiProfileId || null,
239
+ aiProfileIdentityAsserted: Boolean(bootstrap.aiProfileName || bootstrap.aiProfileProvider),
186
240
  aiProfileToken: bootstrap.aiProfileToken || oauthAuth?.aiProfileToken || null,
187
241
  aiProfileName: bootstrap.aiProfileName || oauthAuth?.aiProfileName || null,
188
242
  mcpClientId: oauthAuth
@@ -337,6 +391,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
337
391
  };
338
392
  return async function handleCloudMcpRequest(req, res) {
339
393
  const requestStartedAtMs = Date.now();
394
+ let authMs = 0;
395
+ let bodyParseMs = 0;
396
+ let rateLimitMs = 0;
340
397
  if (requestRuntimeMode(req) !== 'cloud') {
341
398
  writeMcpHttpError(res, 400, 'The /mcp HTTP endpoint is only available in cloud runtime. In local runtime, use the local stdio MCP server (for example: `taskforce mcp <project-path>`).');
342
399
  return;
@@ -347,7 +404,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
347
404
  writeMcpHttpError(res, 405, 'Method not allowed.');
348
405
  return;
349
406
  }
407
+ const authStartedAtMs = Date.now();
350
408
  const auth = authenticateCloudMcpRequest(req, res);
409
+ authMs = Date.now() - authStartedAtMs;
351
410
  if (!auth)
352
411
  return;
353
412
  if (req.method !== 'POST') {
@@ -356,7 +415,9 @@ export function makeHandleCloudMcpRequest(core, deps) {
356
415
  }
357
416
  let body = undefined;
358
417
  try {
418
+ const bodyParseStartedAtMs = Date.now();
359
419
  body = await parseJsonBody(req);
420
+ bodyParseMs = Date.now() - bodyParseStartedAtMs;
360
421
  }
361
422
  catch (error) {
362
423
  writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
@@ -364,6 +425,7 @@ export function makeHandleCloudMcpRequest(core, deps) {
364
425
  }
365
426
  const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
366
427
  const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
428
+ res.setHeader('X-Request-ID', requestId);
367
429
  const mcpMethod = !Array.isArray(body) && body !== null && typeof body === 'object'
368
430
  ? String(body.method || '')
369
431
  : '';
@@ -389,7 +451,11 @@ export function makeHandleCloudMcpRequest(core, deps) {
389
451
  writeMcpHttpError(res, 403, 'Forbidden.');
390
452
  return;
391
453
  }
392
- if (!applyCloudMcpWriteRateLimit(req, res, auth, body))
454
+ const writeToolNames = extractMcpWriteToolNames(body);
455
+ const rateLimitStartedAtMs = Date.now();
456
+ const rateLimitAllowed = applyCloudMcpWriteRateLimit(req, res, auth, body);
457
+ rateLimitMs = Date.now() - rateLimitStartedAtMs;
458
+ if (!rateLimitAllowed)
393
459
  return;
394
460
  let runtime = null;
395
461
  let runtimeConstructionMs = 0;
@@ -397,6 +463,7 @@ export function makeHandleCloudMcpRequest(core, deps) {
397
463
  let requestHandlingMs = 0;
398
464
  let cleanupMs = 0;
399
465
  let outcome = 'success';
466
+ const toolTimingState = { value: null };
400
467
  try {
401
468
  const runtimeStartedAtMs = Date.now();
402
469
  // Intentionally isolate the MCP runtime per HTTP request. The runtime closes over
@@ -420,6 +487,25 @@ export function makeHandleCloudMcpRequest(core, deps) {
420
487
  requestId,
421
488
  authScopes,
422
489
  }),
490
+ onToolCallTiming: (timing) => {
491
+ toolTimingState.value = timing;
492
+ if (!res.headersSent) {
493
+ setServerTimingHeader(res, [
494
+ { name: 'auth', durationMs: authMs },
495
+ { name: 'body-parse', durationMs: bodyParseMs },
496
+ { name: 'rate-limit', durationMs: rateLimitMs },
497
+ { name: 'runtime', durationMs: runtimeConstructionMs },
498
+ { name: 'transport-connect', durationMs: transportConnectMs },
499
+ { name: 'tool-envelope-prepare', durationMs: timing.preparationMs },
500
+ { name: 'tool-envelope-validate', durationMs: timing.validationMs },
501
+ { name: 'tool-handler', durationMs: timing.handlerMs },
502
+ ...Object.entries(timing.subphases).map(([phase, durationMs]) => ({
503
+ name: `tool-${phase}`,
504
+ durationMs,
505
+ })),
506
+ ]);
507
+ }
508
+ },
423
509
  });
424
510
  runtimeConstructionMs = Date.now() - runtimeStartedAtMs;
425
511
  const transport = new StreamableHTTPServerTransport({
@@ -465,7 +551,8 @@ export function makeHandleCloudMcpRequest(core, deps) {
465
551
  cleanupMs = Date.now() - cleanupStartedAtMs;
466
552
  }
467
553
  const totalDurationMs = Date.now() - requestStartedAtMs;
468
- if (initializeRequest || mcpMethod === 'tools/list' || totalDurationMs >= 250) {
554
+ if (initializeRequest || mcpMethod === 'tools/list' || writeToolNames.length > 0 || totalDurationMs >= 250) {
555
+ const timingSnapshot = toolTimingState.value;
469
556
  console.info(`[Taskforce MCP] ${JSON.stringify({
470
557
  event: 'cloud_request_timing',
471
558
  requestId,
@@ -477,13 +564,23 @@ export function makeHandleCloudMcpRequest(core, deps) {
477
564
  method: mcpMethod || null,
478
565
  toolName: extractFirstMcpToolName(body),
479
566
  workspaceId: auth.workspaceId,
567
+ databaseProvider: core.getDatabaseProvider(),
568
+ authMode: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
569
+ batchSize: Array.isArray(body) ? body.length : 1,
570
+ isWrite: writeToolNames.length > 0,
480
571
  outcome,
572
+ httpStatus: res.statusCode,
573
+ responseBytes: timingSnapshot?.responseBytes ?? null,
481
574
  totalDurationMs,
482
575
  preRuntimeMs: Math.max(0, totalDurationMs - runtimeConstructionMs - transportConnectMs - requestHandlingMs - cleanupMs),
576
+ authMs,
577
+ bodyParseMs,
578
+ rateLimitMs,
483
579
  runtimeConstructionMs,
484
580
  transportConnectMs,
485
581
  requestHandlingMs,
486
582
  cleanupMs,
583
+ toolTiming: timingSnapshot,
487
584
  })}`);
488
585
  }
489
586
  }
@@ -521,6 +618,17 @@ export function requestSystemRole(req) {
521
618
  export function requestUserId(req) {
522
619
  return requestHeader(req, 'x-taskforce-user-id', 'anonymous') || 'anonymous';
523
620
  }
621
+ /**
622
+ * Resolve the human actor recorded by ordinary HTTP mutations. The standalone
623
+ * server has already replaced this header with authenticated cloud identity
624
+ * when available; local signed-in clients provide the same account id through
625
+ * the request-scoped mutation header. Anonymous sessions retain the legacy
626
+ * generic actor for compatibility.
627
+ */
628
+ export function requestHumanActorRef(req) {
629
+ const userId = String(requestUserId(req) || '').trim();
630
+ return userId && userId !== 'anonymous' ? userId : 'user';
631
+ }
524
632
  // === SECTION: Workspace ID resolution ===
525
633
  export function makeRequestWorkspaceId(core, context) {
526
634
  return function requestWorkspaceId(req) {
@@ -625,23 +733,11 @@ export function makeEnsureAppAccess(core, context) {
625
733
  if (!access.authenticated || !access.userId || access.userId === 'anonymous')
626
734
  return true;
627
735
  const accountAccess = core.getAccountAccessStatus(access.userId);
628
- if (accountAccess.canAccessApp)
736
+ const denied = resolveAppAccessDeniedResponse(accountAccess);
737
+ if (!denied)
629
738
  return true;
630
- const code = accountAccess.gate === 'plan_selection_required'
631
- ? 'PLAN_SELECTION_REQUIRED'
632
- : accountAccess.gate === 'checkout_pending'
633
- ? 'CHECKOUT_PENDING'
634
- : accountAccess.gate === 'misconfigured'
635
- ? 'ACCOUNT_ACCESS_MISCONFIGURED'
636
- : 'ACCOUNT_ENTITLEMENT_REQUIRED';
637
- res.writeHead(403, { 'Content-Type': 'application/json' });
638
- res.end(JSON.stringify({
639
- error: accountAccess.message || 'This account cannot access the app.',
640
- code,
641
- gate: accountAccess.gate,
642
- canAccessPlans: accountAccess.canAccessPlans,
643
- planSelectionRequired: accountAccess.planSelectionRequired
644
- }));
739
+ res.writeHead(denied.statusCode, { 'Content-Type': 'application/json' });
740
+ res.end(JSON.stringify(denied.body));
645
741
  return false;
646
742
  };
647
743
  }
@@ -2,6 +2,7 @@ import { parseJsonBody } from './primitives.js';
2
2
  import { resolveSessionCookieAttributesForRequest } from '../auth.js';
3
3
  import { isAiProfileSurfaceType, parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
4
4
  import { isAiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
5
+ import { assertPlanningIdentityColor, assertPlanningIdentityIcon } from '../../shared/planningIdentity.js';
5
6
  import { serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload, } from '../../sync/contentSyncPayload.js';
6
7
  import { normalizeAiProfileSyncPayload, normalizeAgentConversationSyncPayload, normalizeAgentRoleSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, normalizeTaskforceAgentSkillSyncPayload, normalizeTaskforceAgentSyncPayload, } from '../../sync/collaborationSyncPayload.js';
7
8
  import { normalizeInitiativeSyncPayload, normalizeTaxonomySyncPayload, normalizeWorkstreamSyncPayload, } from '../../sync/planningSyncPayload.js';
@@ -451,6 +452,27 @@ export function registerSyncRoutes(deps) {
451
452
  op: 'workstream-upsert',
452
453
  workstream
453
454
  })),
455
+ ...[
456
+ ...incomingInitiatives.map((entity) => ({
457
+ entityType: 'initiative',
458
+ entityId: String(entity.id || '').trim(),
459
+ icon: assertPlanningIdentityIcon(entity.icon),
460
+ color: assertPlanningIdentityColor(entity.color),
461
+ updatedAt: String(entity.identityUpdatedAt || '').trim(),
462
+ })),
463
+ ...incomingWorkstreams.map((entity) => ({
464
+ entityType: 'workstream',
465
+ entityId: String(entity.id || '').trim(),
466
+ icon: assertPlanningIdentityIcon(entity.icon),
467
+ color: assertPlanningIdentityColor(entity.color),
468
+ updatedAt: String(entity.identityUpdatedAt || '').trim(),
469
+ })),
470
+ ]
471
+ .filter((identity) => identity.entityId.length > 0 && Number.isFinite(Date.parse(identity.updatedAt)))
472
+ .map((identity) => ({
473
+ op: 'planning-identity-upsert',
474
+ identity,
475
+ })),
454
476
  ...incomingAiProfiles
455
477
  .map((profile) => normalizeAiProfileSyncPayload(profile, cloudWorkspaceId, normalizeIncomingAiProfileSurfaceType, normalizeIncomingAiProfileSeatScope, 'push.aiProfileUpsert'))
456
478
  .filter((profile) => Boolean(profile && profile.id.length > 0))
@@ -50,8 +50,11 @@ export function registerWorkspaceSyncAuxRoutes({ addRoute, core, context, resolv
50
50
  res.end(JSON.stringify({ success: true }));
51
51
  });
52
52
  // POST /api/taskforce/sync/workspace/repair-startup - Auto-repair workspace asset/document
53
- // metadata on workspace open. Safe fixups re-register metadata for local files and mark
54
- // stale, unreferenced metadata deleted when the file is missing. Never deletes files.
53
+ // metadata on workspace open. Safe fixups re-register metadata for local files and clean up
54
+ // stale links. Does NOT tombstone metadata for files that appear missing locally a single
55
+ // failed existence check at startup can mean the content is only temporarily unavailable
56
+ // (e.g. storage not yet mounted/synced), not deleted; that requires explicit admin
57
+ // confirmation via /api/taskforce/admin/workspace-repair/apply. Never deletes files.
55
58
  // Called once by the sync orchestrator when the workspace becomes active.
56
59
  addRoute('POST', '/api/taskforce/sync/workspace/repair-startup', async (req, res) => {
57
60
  const authConfig = context.authConfig;
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_WORKSPACE_PULL_PAGE_MAX_BYTES, buildWorkspacePullFeed, WorkspacePullEntryTooLargeError, } from '../../sync/workspacePullFeed.js';
2
2
  import { applyWorkspaceSyncLocalBatch } from '../../sync/engine/workspaceSyncEngineLocalApply.js';
3
3
  import { parseJsonBody } from './primitives.js';
4
- import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
4
+ import { normalizeWorkspaceSyncV2ProjectionExclusions, parseWorkspaceSyncV2ProjectionExclusions, WORKSPACE_SYNC_V3_COMBINED_V2_EXCLUSIONS, workspaceSyncV2ProjectionExclusionsEqual, } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
5
5
  import { resolveWorkspaceSyncV3AttestedV2Exclusions } from '../../sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js';
6
6
  import { requireAuthenticatedSyncRequest, requireEnabledSyncAuth, resolveAuthorizedSyncWorkspace, } from './syncRouteGuards.js';
7
7
  import { completeLocalSyncBrowserPushEchoPermitGroup, createLocalSyncBrowserV2ApplyTransactionHooks, parseLocalSyncBrowserApplyPermitContext, resolveLocalSyncBrowserApplyPermitOperationClass, } from '../../sync/coordinator/localSyncBrowserApplyPermitContext.js';
@@ -301,7 +301,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
301
301
  };
302
302
  }
303
303
  if (!change) {
304
- missing.push(request);
304
+ missing.push({
305
+ ...request,
306
+ disposition: request.kind === 'asset'
307
+ && 'assetId' in request
308
+ && (!requestedAsset
309
+ || requestedAsset.deletedAt
310
+ || requestedAsset.storageProvider === 'external')
311
+ ? 'terminal-unavailable'
312
+ : 'retryable',
313
+ });
305
314
  consumed += 1;
306
315
  continue;
307
316
  }
@@ -554,6 +563,16 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
554
563
  : null;
555
564
  let applyResult;
556
565
  if (permitContext && coordinatorServices && permitOperationClass === 'push') {
566
+ const attestedV2ExcludedProjections = v2ExcludedProjections.length > 0
567
+ ? (() => {
568
+ const attested = resolveWorkspaceSyncV3AttestedV2Exclusions({
569
+ coverage: body?.combinedCoverage,
570
+ excludedProjections: v2ExcludedProjections,
571
+ runtimeMode: 'local',
572
+ });
573
+ return attested && workspaceSyncV2ProjectionExclusionsEqual(attested, v2ExcludedProjections) ? attested : null;
574
+ })()
575
+ : [];
557
576
  applyResult = completeLocalSyncBrowserPushEchoPermitGroup({
558
577
  services: coordinatorServices,
559
578
  cookieHeader: req.headers.cookie,
@@ -567,13 +586,13 @@ export function registerWorkspacePullApplySyncRoutes({ addRoute, core, context,
567
586
  || workspaceMembers !== undefined
568
587
  || body?.bootstrapSnapshot !== undefined
569
588
  || allowReferenceNumberReassignment
570
- || v2ExcludedProjections.length > 0) {
589
+ || !attestedV2ExcludedProjections) {
571
590
  throw new Error('Browser push echo apply accepts only authoritative task upserts.');
572
591
  }
573
592
  return dispatchWorkspaceSyncMetadataAtomically(workspaceId, incomingChanges, {
574
593
  allowReferenceNumberReassignment: false,
575
594
  coexistenceDirection: 'local-pull',
576
- v2ExcludedProjections: [],
595
+ v2ExcludedProjections: attestedV2ExcludedProjections,
577
596
  });
578
597
  },
579
598
  });
@@ -5,7 +5,7 @@ import { buildWorkspaceSyncV3InitiativeRepairPage, decodeWorkspaceSyncV3Initiati
5
5
  import { WORKSPACE_SYNC_V3_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3FeedProfile.js';
6
6
  import { isWorkspaceSyncV3InitiativeLegacyPushDisabled } from '../../sync/v3/planningActivation.js';
7
7
  import { WORKSPACE_SYNC_V3_COMBINED_COVERAGE_PROFILE } from '../../sync/v3/workspaceSyncV3CombinedCoverage.js';
8
- import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
8
+ import { buildWorkspaceSyncV3CombinedRepairPage, decodeWorkspaceSyncV3CombinedRepairCursor, decodeWorkspaceSyncV3NestedRepairCursor, decodeWorkspaceSyncV3PlanningOrderRepairCursor, decodeWorkspaceSyncV3TaskCoverRepairCursor, decodeWorkspaceSyncV3WorkflowRepairCursor, WorkspaceSyncV3CombinedRepairSnapshotUnavailableError, } from '../../sync/v3/workspaceSyncV3CombinedRepairMaterialization.js';
9
9
  import { isWorkspaceSyncV3CombinedLegacyFenceEnabled } from '../../sync/v3/workspaceSyncV2ProjectionExclusions.js';
10
10
  import { resolveCombinedFeedCapability } from '../../sync/v3/combinedFeedCapability.js';
11
11
  import { resolveWorkspaceSyncV3RegisteredCoverage } from '../../sync/v3/workspaceSyncV3CoverageRegistry.js';
@@ -21,6 +21,17 @@ function json(res, status, body) {
21
21
  res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
22
22
  res.end(JSON.stringify(body));
23
23
  }
24
+ function decodeCombinedRepairCursorForContract(rawCursor, keyring, wire) {
25
+ if (wire.taskCoverProjectionRequired)
26
+ return decodeWorkspaceSyncV3TaskCoverRepairCursor(rawCursor, keyring);
27
+ if (wire.workflowWriterPrerequisitesRequired)
28
+ return decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring);
29
+ if (wire.planningOrderWriterPrerequisitesRequired)
30
+ return decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring);
31
+ if (wire.nestedWriterPrerequisitesRequired)
32
+ return decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring);
33
+ return decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
34
+ }
24
35
  function attestLegacyPushBoundary(body, boundary) {
25
36
  const attested = { ...body, ...boundary };
26
37
  const mutable = attested;
@@ -215,13 +226,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
215
226
  return;
216
227
  }
217
228
  const rawCursor = String(url.searchParams.get('cursor') || '').trim();
218
- const cursor = requestedWire.workflowWriterPrerequisitesRequired
219
- ? decodeWorkspaceSyncV3WorkflowRepairCursor(rawCursor, keyring)
220
- : requestedWire.planningOrderWriterPrerequisitesRequired
221
- ? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawCursor, keyring)
222
- : requestedWire.nestedWriterPrerequisitesRequired
223
- ? decodeWorkspaceSyncV3NestedRepairCursor(rawCursor, keyring)
224
- : decodeWorkspaceSyncV3CombinedRepairCursor(rawCursor, keyring);
229
+ const cursor = decodeCombinedRepairCursorForContract(rawCursor, keyring, requestedWire);
225
230
  if (rawCursor && !cursor) {
226
231
  json(res, 400, { success: false, code: 'INVALID_SYNC_COMBINED_REPAIR_CURSOR', error: 'Invalid combined repair cursor.' });
227
232
  return;
@@ -231,7 +236,8 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
231
236
  cursorKeyring: keyring, cursor, pageSize: Number.isFinite(limit) ? limit : undefined,
232
237
  nestedCoverage: requestedWire.nestedWriterPrerequisitesRequired,
233
238
  workflowCoverage: requestedWire.workflowWriterPrerequisitesRequired,
234
- planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired });
239
+ planningOrderCoverage: requestedWire.planningOrderWriterPrerequisitesRequired,
240
+ taskCoverCoverage: requestedWire.taskCoverProjectionRequired });
235
241
  json(res, 200, attestLegacyPushBoundary({ ...page }, {
236
242
  contractVersion: 3,
237
243
  legacyV2Boundary: { cloudWritesFenced: false, excludedProjections: [] },
@@ -328,13 +334,7 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
328
334
  return;
329
335
  }
330
336
  const repairCursor = coverage.kind === 'combined'
331
- ? (coverage.wire.workflowWriterPrerequisitesRequired
332
- ? decodeWorkspaceSyncV3WorkflowRepairCursor(rawRepairCursor, cursorKeyring)
333
- : coverage.wire.planningOrderWriterPrerequisitesRequired
334
- ? decodeWorkspaceSyncV3PlanningOrderRepairCursor(rawRepairCursor, cursorKeyring)
335
- : coverage.wire.nestedWriterPrerequisitesRequired
336
- ? decodeWorkspaceSyncV3NestedRepairCursor(rawRepairCursor, cursorKeyring)
337
- : decodeWorkspaceSyncV3CombinedRepairCursor(rawRepairCursor, cursorKeyring))
337
+ ? decodeCombinedRepairCursorForContract(rawRepairCursor, cursorKeyring, coverage.wire)
338
338
  : decodeWorkspaceSyncV3InitiativeRepairCursor(rawRepairCursor, cursorKeyring);
339
339
  if (rawRepairCursor && (!repairEnabled || !repairCursor)) {
340
340
  json(res, 400, { success: false, code: 'INVALID_SYNC_V3_REPAIR_CURSOR', error: 'Invalid repair cursor.' });
@@ -352,7 +352,9 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
352
352
  pageSize: Number.isFinite(limit) ? limit : undefined,
353
353
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
354
354
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
355
- planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired })
355
+ planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
356
+ taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
357
+ coveredLegacyWritesFenced: combinedLegacyPushDisabled })
356
358
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
357
359
  cursorKeyring: cursorKeyring, cursor: repairCursor,
358
360
  pageSize: Number.isFinite(limit) ? limit : undefined });
@@ -397,7 +399,9 @@ export function registerWorkspaceSyncV3FeedRoutes(input) {
397
399
  cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined,
398
400
  nestedCoverage: coverage.wire.nestedWriterPrerequisitesRequired,
399
401
  workflowCoverage: coverage.wire.workflowWriterPrerequisitesRequired,
400
- planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired })
402
+ planningOrderCoverage: coverage.wire.planningOrderWriterPrerequisitesRequired,
403
+ taskCoverCoverage: coverage.wire.taskCoverProjectionRequired,
404
+ coveredLegacyWritesFenced: combinedLegacyPushDisabled })
401
405
  : buildWorkspaceSyncV3InitiativeRepairPage({ core: input.core, workspaceId,
402
406
  cursorKeyring: cursorKeyring, pageSize: Number.isFinite(limit) ? limit : undefined });
403
407
  const repair = coverage.kind === 'combined' ? page : { ...page,