@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
@@ -16,7 +16,7 @@ import { AnnotatedAttachmentCommandError, createAnnotatedAttachmentCommandServic
16
16
  import { createDocumentReviewCommandService, DocumentReviewCommandError, } from '../documentReviews/commandService.js';
17
17
  import { COMPLEXITY_OPTIONS, resolveNumberOption, resolveStringOption, STATUS_OPTIONS } from './optionResolution.js';
18
18
  import { buildResolveAiProfileInputFromBootstrap } from './aiProfileBootstrap.js';
19
- import { createCanonicalAssetHelpers } from './canonicalAssetHelpers.js';
19
+ import { CanonicalDocumentHeadStaleError, createCanonicalAssetHelpers } from './canonicalAssetHelpers.js';
20
20
  import { createDocumentHelpers } from './documentHelpers.js';
21
21
  import { createTaskAttachmentHelpers } from './taskAttachmentHelpers.js';
22
22
  import { listRegisteredGitWorktreeRoots } from './gitWorktreeRoots.js';
@@ -32,7 +32,8 @@ import { WorkspaceSyncContentVersionStore, } from '../sync/engine/workspaceSyncC
32
32
  import { WorkspaceSyncPublishedContentResolver } from '../sync/engine/workspaceSyncPublishedContentResolver.js';
33
33
  import { createWorkspaceSyncServerContentRuntime } from '../sync/engine/workspaceSyncServerContentRuntime.js';
34
34
  import { resolveVirusScanConfigFromEnv, scanBufferForThreats } from '../security/virusScan.js';
35
- import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, resolveAppGateAccess, } from '../runtime/appGates.js';
35
+ import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, } from '../runtime/appGates.js';
36
+ import { resolveNodeAppGateAccess } from '../runtime/nodeAppGates.js';
36
37
  import { getDocumentReferenceLabel, parseDocumentReference } from '../utils/documentReferences.js';
37
38
  import { getImageReferenceLabel, parseImageReference } from '../utils/imageReferences.js';
38
39
  import { inferCanonicalAssetKind, isValidCanonicalDocumentAsset } from '../utils/canonicalAssetKind.js';
@@ -59,7 +60,7 @@ import { createTaskPlanCommand } from './taskPlanCommand.js';
59
60
  import { WorkflowAssignmentService } from '../core/WorkflowAssignmentService.js';
60
61
  import { WorkflowExecutionService, WorkflowExecutionServiceError, } from '../core/WorkflowExecutionService.js';
61
62
  import { WorkflowStore } from '../storage/workflowStore.js';
62
- import { getMcpClientRegistryEntry, normalizeMcpConnectionId, parseMcpClientId } from './clientRegistry.js';
63
+ import { getMcpClientRegistryEntry, inferLegacyMcpClientIdFromProfile, normalizeMcpConnectionId, parseMcpClientId } from './clientRegistry.js';
63
64
  import { BedrockAvatarImageProvider } from '../services/avatarImageProvider.js';
64
65
  import { AiProfileAvatarGenerationError, checkAiProfileAvatarGenerationRateLimit, generateAiProfileAvatar, } from '../services/aiProfileAvatarGenerationService.js';
65
66
  import { isAiProfileAvatarGenerationEligible } from '../shared/aiProfileAvatarEligibility.js';
@@ -410,6 +411,22 @@ function resolveRecoverableToolError(error, context) {
410
411
  };
411
412
  }
412
413
  if (error instanceof TaskforceRuleError) {
414
+ if (code === 'AI_PROFILE_SELECTION_REQUIRED') {
415
+ const selectionDetails = details && typeof details === 'object' && !Array.isArray(details)
416
+ ? details
417
+ : {};
418
+ const suggestedArgs = selectionDetails.suggestedArgs;
419
+ return {
420
+ message,
421
+ code,
422
+ details,
423
+ nextTool: 'resolve_profile',
424
+ ...(suggestedArgs && typeof suggestedArgs === 'object' && !Array.isArray(suggestedArgs)
425
+ ? { suggestedArgs: suggestedArgs }
426
+ : {}),
427
+ retryHint: 'Choose the intended compatible profile candidate and retry resolve_profile with the same name plus its profileId. If more than one candidate is present and the intended identity is unclear, ask the user before selecting.',
428
+ };
429
+ }
413
430
  if (code === 'AI_PROFILE_AVATAR_PROVIDER_FAILED' || code === 'AI_PROFILE_AVATAR_PERSISTENCE_FAILED') {
414
431
  return {
415
432
  message,
@@ -677,6 +694,14 @@ export function buildUnexpectedToolErrorResult(requestId) {
677
694
  isError: true,
678
695
  };
679
696
  }
697
+ function measureToolResultBytes(result) {
698
+ try {
699
+ return Buffer.byteLength(JSON.stringify(result), 'utf8');
700
+ }
701
+ catch {
702
+ return 0;
703
+ }
704
+ }
680
705
  export function buildBatchToolError(error, fallbackStatusCode) {
681
706
  const resolved = resolveRecoverableToolError(error);
682
707
  if (!resolved) {
@@ -786,6 +811,29 @@ export function createTaskforceMcpServer(options = {}) {
786
811
  }
787
812
  const mcpRequestIdentity = new AsyncLocalStorage();
788
813
  const cloudCreateRequestIdentity = new AsyncLocalStorage();
814
+ const mcpToolSubphaseTimings = new AsyncLocalStorage();
815
+ const measureToolSubphase = (phase, operation) => {
816
+ const startedAtMs = Date.now();
817
+ try {
818
+ return operation();
819
+ }
820
+ finally {
821
+ const timings = mcpToolSubphaseTimings.getStore();
822
+ if (timings)
823
+ timings[phase] = (timings[phase] || 0) + (Date.now() - startedAtMs);
824
+ }
825
+ };
826
+ const measureToolSubphaseAsync = async (phase, operation) => {
827
+ const startedAtMs = Date.now();
828
+ try {
829
+ return await operation();
830
+ }
831
+ finally {
832
+ const timings = mcpToolSubphaseTimings.getStore();
833
+ if (timings)
834
+ timings[phase] = (timings[phase] || 0) + (Date.now() - startedAtMs);
835
+ }
836
+ };
789
837
  const operationIdentityScope = options.operationIdentityScope
790
838
  ? {
791
839
  generation: String(options.operationIdentityScope.generation || '').trim(),
@@ -824,28 +872,34 @@ export function createTaskforceMcpServer(options = {}) {
824
872
  return null;
825
873
  return `${kind}-${createHash('sha256').update(`${kind}:${requestIdentity}:${discriminator || ''}`).digest('hex').slice(0, 32)}`;
826
874
  };
827
- const stableTaskCreateIdempotencyId = (kind, idempotencyKey, discriminator) => {
828
- const fingerprint = operationIdentityScope
829
- ? fingerprintWorkspaceSyncPayload({
830
- kind,
831
- tenantId,
832
- workspaceId: ACTIVE_WORKSPACE_ID,
833
- actorRef: getActiveMcpActorRef(),
834
- tool: 'create_task',
835
- idempotencyKey,
836
- discriminator: discriminator || '',
837
- })
838
- : createHash('sha256').update(JSON.stringify([
839
- kind,
840
- tenantId,
841
- ACTIVE_WORKSPACE_ID,
842
- getActiveMcpActorRef(),
843
- 'create_task',
844
- idempotencyKey,
845
- discriminator || '',
846
- ])).digest('hex');
875
+ const normalizeExplicitIdempotencyKey = (value) => {
876
+ if (value === undefined || value === null)
877
+ return null;
878
+ const key = String(value).trim();
879
+ if (!key)
880
+ throw new McpPublicError('idempotencyKey must contain non-whitespace text.');
881
+ if (key.length > 128)
882
+ throw new McpPublicError('idempotencyKey must contain at most 128 characters.');
883
+ return key;
884
+ };
885
+ const stableExplicitMutationId = (kind, tool, idempotencyKey, discriminator) => {
886
+ const fingerprint = createHash('sha256').update(JSON.stringify([
887
+ kind,
888
+ tenantId,
889
+ ACTIVE_WORKSPACE_ID,
890
+ getActiveMcpActorRef(),
891
+ tool,
892
+ idempotencyKey,
893
+ discriminator || '',
894
+ ])).digest('hex');
847
895
  return `${kind}-${fingerprint.slice(0, 32)}`;
848
896
  };
897
+ const stableTaskCreateIdempotencyId = (kind, idempotencyKey, discriminator) => stableExplicitMutationId(kind, 'create_task', idempotencyKey, discriminator);
898
+ const requireDurableExplicitIdempotency = (enabled, tool) => {
899
+ if (enabled)
900
+ return;
901
+ throw new McpPublicError(`idempotencyKey requires the durable ${tool} mutation boundary, which is unavailable for this runtime.`, 'MCP_IDEMPOTENCY_UNAVAILABLE', 503);
902
+ };
849
903
  const durableTaskMutationRouter = options.durableTaskMutationRouter || createDurableTaskMutationRouter({
850
904
  core,
851
905
  runtimeMode,
@@ -929,7 +983,8 @@ export function createTaskforceMcpServer(options = {}) {
929
983
  cloudClientId: 'mcp-cloud',
930
984
  createOperationId: () => mutationOptions?.operationId
931
985
  || requireStableMcpRequestId('operation', 'initiative'),
932
- createCommentId: () => requireStableMcpRequestId('comment', 'initiative-comment'),
986
+ createCommentId: () => mutationOptions?.commentId
987
+ || requireStableMcpRequestId('comment', 'initiative-comment'),
933
988
  deferPostCommitNotification: mutationOptions?.deferPostCommitNotification,
934
989
  });
935
990
  const initiativeFromDurableResult = (result) => result.state === 'pending' ? result.provisionalInitiative : result.authoritativeInitiative;
@@ -946,7 +1001,8 @@ export function createTaskforceMcpServer(options = {}) {
946
1001
  cloudClientId: 'mcp-cloud',
947
1002
  createOperationId: () => mutationOptions?.operationId
948
1003
  || requireStableMcpRequestId('operation', 'workstream'),
949
- createCommentId: () => requireStableMcpRequestId('comment', 'workstream-comment'),
1004
+ createCommentId: () => mutationOptions?.commentId
1005
+ || requireStableMcpRequestId('comment', 'workstream-comment'),
950
1006
  deferPostCommitNotification: mutationOptions?.deferPostCommitNotification,
951
1007
  });
952
1008
  const workstreamFromDurableResult = (result) => result.state === 'pending' ? result.provisionalWorkstream : result.authoritativeWorkstream;
@@ -1049,7 +1105,9 @@ export function createTaskforceMcpServer(options = {}) {
1049
1105
  legacyProvider: asset.storageProvider,
1050
1106
  });
1051
1107
  return resolved.source === 'versioned'
1052
- && resolved.contentSha256 === asset.contentSha256;
1108
+ && resolved.storageProvider === asset.storageProvider
1109
+ && resolved.contentSha256 === asset.contentSha256
1110
+ && resolved.sizeBytes === asset.sizeBytes;
1053
1111
  }
1054
1112
  catch {
1055
1113
  return false;
@@ -1151,7 +1209,9 @@ export function createTaskforceMcpServer(options = {}) {
1151
1209
  },
1152
1210
  tools: {},
1153
1211
  },
1154
- instructions: `Before writes, check get_current_profile; if unbound, use resolve_profile. Keep profileToken private. Use T-, IN-, WS-, I-, and D- references. Link url as [reference](url). For tracked work, prefer start_task and finish_task.`,
1212
+ instructions: runtimeMode === 'cloud' && options.auth?.actorType === 'token'
1213
+ ? `Cloud access-token clients must pass profileToken on every write; obtain it from resolve_profile and keep it private. Use T-, IN-, WS-, I-, and D- references. Link url as [reference](url). For tracked work, prefer start_task and finish_task.`
1214
+ : `Before writes, check get_current_profile; if unbound, use resolve_profile. Keep profileToken private. Use T-, IN-, WS-, I-, and D- references. Link url as [reference](url). For tracked work, prefer start_task and finish_task.`,
1155
1215
  });
1156
1216
  const TOOL_PERMISSIONS = MCP_TOOL_PERMISSIONS;
1157
1217
  const RESOURCE_PERMISSIONS = {
@@ -1243,14 +1303,21 @@ export function createTaskforceMcpServer(options = {}) {
1243
1303
  }
1244
1304
  return { id: task.id, task };
1245
1305
  }
1246
- const buildDocumentDescriptor = (asset, options) => (withDocumentUrl(documentHelpers.buildDocumentDescriptor(asset, options)));
1306
+ const buildDocumentDescriptor = (asset, options) => {
1307
+ const descriptor = withDocumentUrl(documentHelpers.buildDocumentDescriptor(asset, options));
1308
+ if (runtimeMode === 'local')
1309
+ return descriptor;
1310
+ const { storageKey: _storageKey, ...cloudDescriptor } = descriptor;
1311
+ return cloudDescriptor;
1312
+ };
1247
1313
  const buildCanonicalDocumentAttachmentHint = documentHelpers.buildCanonicalDocumentAttachmentHint;
1248
1314
  function buildBoundedTextContent(content, maxChars) {
1249
1315
  const requestedMaxChars = Number(maxChars);
1250
1316
  const hasRequestedMaxChars = maxChars !== undefined && maxChars !== null && maxChars !== '';
1251
- const resolvedMaxChars = hasRequestedMaxChars && Number.isFinite(requestedMaxChars)
1252
- ? Math.max(1, Math.floor(requestedMaxChars))
1253
- : 200000;
1317
+ if (hasRequestedMaxChars && (!Number.isInteger(requestedMaxChars) || requestedMaxChars < 1)) {
1318
+ throw new McpPublicError('maxChars must be a positive integer');
1319
+ }
1320
+ const resolvedMaxChars = hasRequestedMaxChars ? requestedMaxChars : 200000;
1254
1321
  const characters = Array.from(content);
1255
1322
  const totalChars = characters.length;
1256
1323
  const boundedContent = totalChars > resolvedMaxChars
@@ -1271,7 +1338,19 @@ export function createTaskforceMcpServer(options = {}) {
1271
1338
  format: "metadata"
1272
1339
  };
1273
1340
  }
1341
+ const assertCurrentHead = () => {
1342
+ try {
1343
+ canonicalAssetHelpers.assertCanonicalDocumentHeadCurrent(asset);
1344
+ }
1345
+ catch (error) {
1346
+ if (error instanceof CanonicalDocumentHeadStaleError) {
1347
+ throw new McpPublicError(error.message, error.code, error.statusCode, error.details);
1348
+ }
1349
+ throw error;
1350
+ }
1351
+ };
1274
1352
  if (format === "download") {
1353
+ assertCurrentHead();
1275
1354
  return {
1276
1355
  format: "download",
1277
1356
  downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
@@ -1279,6 +1358,7 @@ export function createTaskforceMcpServer(options = {}) {
1279
1358
  };
1280
1359
  }
1281
1360
  if (!isTextReadableMimeType(asset.mimeType || '')) {
1361
+ assertCurrentHead();
1282
1362
  return {
1283
1363
  format: "download",
1284
1364
  downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
@@ -1290,6 +1370,9 @@ export function createTaskforceMcpServer(options = {}) {
1290
1370
  buffer = await canonicalAssetHelpers.readCanonicalDocumentBuffer(asset);
1291
1371
  }
1292
1372
  catch (error) {
1373
+ if (error instanceof CanonicalDocumentHeadStaleError) {
1374
+ throw new McpPublicError(error.message, error.code, error.statusCode, error.details);
1375
+ }
1293
1376
  const message = error instanceof Error ? String(error.message || '').trim() : '';
1294
1377
  if (/^Document .+ not found in workspace /i.test(message)) {
1295
1378
  throw new McpPublicError(message, 'MCP_NOT_FOUND', 404);
@@ -1326,8 +1409,10 @@ export function createTaskforceMcpServer(options = {}) {
1326
1409
  }
1327
1410
  const requestedLimit = Number(options?.limit);
1328
1411
  const limit = Number.isFinite(requestedLimit)
1329
- ? Math.max(1, Math.min(50, Math.trunc(requestedLimit)))
1412
+ ? Math.max(1, Math.min(51, Math.trunc(requestedLimit)))
1330
1413
  : 10;
1414
+ const requestedOffset = Number(options?.offset);
1415
+ const offset = Number.isFinite(requestedOffset) ? Math.max(0, Math.trunc(requestedOffset)) : 0;
1331
1416
  const normalize = (value) => typeof value === 'string' ? value.trim().toLowerCase() : '';
1332
1417
  const tokenize = (value) => normalize(value).split(/[^a-z0-9]+/i).filter(Boolean);
1333
1418
  const allImages = workspaceAssetStore.listAllByWorkspace(ACTIVE_WORKSPACE_ID, { kind: 'image' });
@@ -1371,8 +1456,10 @@ export function createTaskforceMcpServer(options = {}) {
1371
1456
  return [{ asset, matchedOn: Array.from(matchedOn), score }];
1372
1457
  });
1373
1458
  return scoredMatches
1374
- .sort((a, b) => b.score - a.score || String(b.asset.updatedAt || '').localeCompare(String(a.asset.updatedAt || '')))
1375
- .slice(0, limit);
1459
+ .sort((a, b) => b.score - a.score
1460
+ || String(b.asset.updatedAt || '').localeCompare(String(a.asset.updatedAt || ''))
1461
+ || a.asset.assetId.localeCompare(b.asset.assetId))
1462
+ .slice(offset, offset + limit);
1376
1463
  }
1377
1464
  const searchDocumentAssets = documentHelpers.searchDocumentAssets;
1378
1465
  function buildImageDescriptor(asset) {
@@ -1384,9 +1471,11 @@ export function createTaskforceMcpServer(options = {}) {
1384
1471
  originalFilename: asset.originalFilename ?? null,
1385
1472
  mimeType: asset.mimeType ?? null,
1386
1473
  sizeBytes: typeof asset.sizeBytes === 'number' ? asset.sizeBytes : null,
1387
- storageKey: asset.storageKey ?? null,
1474
+ ...(runtimeMode === 'local' ? { storageKey: asset.storageKey ?? null } : {}),
1388
1475
  updatedAt: asset.updatedAt ?? null,
1389
- apiUrl: `/api/taskforce/context/${encodeURIComponent(String(asset.storageKey || '').trim())}`,
1476
+ ...(runtimeMode === 'local'
1477
+ ? { apiUrl: `/api/taskforce/context/${encodeURIComponent(String(asset.storageKey || '').trim())}` }
1478
+ : {}),
1390
1479
  };
1391
1480
  return withAttachmentUrl({ ...descriptor, kind: 'image' });
1392
1481
  }
@@ -1396,6 +1485,9 @@ export function createTaskforceMcpServer(options = {}) {
1396
1485
  }
1397
1486
  catch (error) {
1398
1487
  const requestedIdentifier = typeof documentIdentifier === 'string' ? documentIdentifier.trim() : '';
1488
+ if (!requestedIdentifier) {
1489
+ throw new McpPublicError('documentId is required', 'MCP_INVALID_ARGUMENT', 400);
1490
+ }
1399
1491
  if (requestedIdentifier && /not found in workspace/i.test(String(error?.message || error))) {
1400
1492
  throw new TaskforceRuleError(String(error?.message || error), 404, 'DOCUMENT_NOT_FOUND', {
1401
1493
  requestedIdentifier,
@@ -1436,9 +1528,11 @@ export function createTaskforceMcpServer(options = {}) {
1436
1528
  || `Image ${normalizedValue} not found in workspace ${ACTIVE_WORKSPACE_ID}. If you only know a keyword, filename, or rough title, call search_images first.`, 'MCP_NOT_FOUND', 404);
1437
1529
  }
1438
1530
  const auditActorType = options.auth?.actorType || (runtimeMode === 'cloud' ? 'user' : 'local');
1531
+ const requiresExplicitTokenAuthorship = runtimeMode === 'cloud' && auditActorType === 'token';
1439
1532
  const auditActorUserId = String(options.auth?.userId || (runtimeMode === 'cloud' ? 'anonymous' : 'local-mcp')).trim() || 'anonymous';
1440
1533
  const auditTokenId = options.auth?.tokenId ? String(options.auth.tokenId).trim() : null;
1441
1534
  const configuredAiProfileId = options.auth?.aiProfileId ? String(options.auth.aiProfileId).trim() || null : null;
1535
+ const configuredAiProfileIdentityAsserted = options.auth?.aiProfileIdentityAsserted === true;
1442
1536
  const configuredAiProfileToken = options.auth?.aiProfileToken ? String(options.auth.aiProfileToken).trim() || null : null;
1443
1537
  const configuredAiProfileName = options.auth?.aiProfileName ? String(options.auth.aiProfileName).trim() || null : null;
1444
1538
  const configuredAiProfileIcon = options.auth?.aiProfileIcon ? String(options.auth.aiProfileIcon).trim() || null : null;
@@ -1456,11 +1550,59 @@ export function createTaskforceMcpServer(options = {}) {
1456
1550
  const auditUserAgent = options.auth?.userAgent ? String(options.auth.userAgent).trim() || null : null;
1457
1551
  let activeAiProfileId = null;
1458
1552
  let activeAiBindingSource = null;
1553
+ let aiProfileIdentityConflictDetected = false;
1554
+ function failAiProfileBindingConflict(message, details) {
1555
+ aiProfileIdentityConflictDetected = true;
1556
+ let credentialRevoked = false;
1557
+ if (auditActorType === 'oauth' && auditTokenId) {
1558
+ core.revokeMcpOAuthConnectorGrant({
1559
+ actorUserId: auditActorUserId,
1560
+ workspaceId: ACTIVE_WORKSPACE_ID,
1561
+ grantId: auditTokenId,
1562
+ });
1563
+ credentialRevoked = true;
1564
+ }
1565
+ throw new TaskforceRuleError(credentialRevoked ? `${message} The conflicting credential was revoked; reconnect before retrying.` : message, 409, 'AI_PROFILE_BINDING_CONFLICT', { ...details, credentialRevoked, nextTool: 'resolve_profile' });
1566
+ }
1567
+ function assertRequestedIdentityMatchesBoundProfile(profile, input) {
1568
+ if (auditActorType !== 'oauth')
1569
+ return;
1570
+ const requestedName = String(input.name || '').trim();
1571
+ const requestedNameKey = requestedName.toLowerCase();
1572
+ const requestedUsernameKey = normalizeConfiguredAiProfileUsername(undefined, requestedName).toLowerCase();
1573
+ const boundNameKey = String(profile.name || '').trim().toLowerCase();
1574
+ const boundUsernameKey = String(profile.username || '').trim().toLowerCase();
1575
+ const nameMatches = Boolean(requestedNameKey) && (requestedNameKey === boundNameKey
1576
+ || requestedNameKey === boundUsernameKey
1577
+ || requestedUsernameKey === boundUsernameKey);
1578
+ const normalizeProvider = (value) => String(value || '')
1579
+ .trim()
1580
+ .toLowerCase()
1581
+ .replace(/[^a-z0-9]+/g, '');
1582
+ const requestedProvider = normalizeProvider(input.provider);
1583
+ const boundProvider = normalizeProvider(profile.provider);
1584
+ const providerConflicts = Boolean(requestedProvider && boundProvider && requestedProvider !== boundProvider);
1585
+ if (nameMatches && !providerConflicts)
1586
+ return;
1587
+ failAiProfileBindingConflict('The requested AI identity conflicts with the profile already bound to this OAuth connection.', {
1588
+ reason: 'requested_identity_mismatch',
1589
+ boundProfile: {
1590
+ id: profile.id,
1591
+ name: profile.name,
1592
+ username: profile.username,
1593
+ provider: profile.provider || null,
1594
+ },
1595
+ requestedIdentity: {
1596
+ name: requestedName || null,
1597
+ provider: String(input.provider || '').trim() || null,
1598
+ },
1599
+ });
1600
+ }
1459
1601
  function applyConfiguredMcpClientContext(profileId) {
1460
1602
  const shouldBindOAuthGrant = auditActorType === 'oauth'
1461
1603
  && Boolean(auditTokenId)
1462
1604
  && configuredAiProfileId !== profileId;
1463
- const shouldApplyClientContext = Boolean((configuredMcpClientId && configuredMcpClientId !== 'general')
1605
+ const shouldApplyClientContext = auditActorType !== 'token' && Boolean((configuredMcpClientId && configuredMcpClientId !== 'general')
1464
1606
  || configuredMcpConnectionId);
1465
1607
  if (!shouldBindOAuthGrant && !shouldApplyClientContext)
1466
1608
  return;
@@ -1541,6 +1683,7 @@ export function createTaskforceMcpServer(options = {}) {
1541
1683
  workspaceId: ACTIVE_WORKSPACE_ID,
1542
1684
  connectionId: configuredMcpConnectionId,
1543
1685
  seatScope: configuredAiProfileSeatScope,
1686
+ repairLegacyBinding: options?.repairConnection !== false,
1544
1687
  });
1545
1688
  if (!match && auditActorType !== 'oauth' && configuredMcpClientId && configuredMcpClientId !== 'general') {
1546
1689
  const candidates = core.listAiProfiles(ACTIVE_WORKSPACE_ID).filter((profile) => {
@@ -1551,12 +1694,14 @@ export function createTaskforceMcpServer(options = {}) {
1551
1694
  && connection.clientId === configuredMcpClientId);
1552
1695
  });
1553
1696
  if (candidates.length === 1) {
1554
- match = core.bindAiProfileConnection({
1555
- workspaceId: ACTIVE_WORKSPACE_ID,
1556
- profileId: candidates[0].id,
1557
- connectionId: configuredMcpConnectionId,
1558
- clientId: configuredMcpClientId,
1559
- });
1697
+ match = options?.repairConnection === false
1698
+ ? candidates[0]
1699
+ : core.bindAiProfileConnection({
1700
+ workspaceId: ACTIVE_WORKSPACE_ID,
1701
+ profileId: candidates[0].id,
1702
+ connectionId: configuredMcpConnectionId,
1703
+ clientId: configuredMcpClientId,
1704
+ });
1560
1705
  }
1561
1706
  }
1562
1707
  if (match?.archivedAt) {
@@ -1575,6 +1720,8 @@ export function createTaskforceMcpServer(options = {}) {
1575
1720
  mergeTarget = next;
1576
1721
  }
1577
1722
  if (!mergeTarget.archivedAt) {
1723
+ if (options?.repairConnection === false)
1724
+ return mergeTarget;
1578
1725
  return core.bindAiProfileConnection({
1579
1726
  workspaceId: ACTIVE_WORKSPACE_ID,
1580
1727
  profileId: mergeTarget.id,
@@ -1619,10 +1766,14 @@ export function createTaskforceMcpServer(options = {}) {
1619
1766
  return resolved;
1620
1767
  }
1621
1768
  function findTrustedBoundProfile(options) {
1769
+ // A generic access token proves workspace/tool authorization, not which AI
1770
+ // is acting. Never use its historical connection row as authorship proof.
1771
+ if (requiresExplicitTokenAuthorship)
1772
+ return null;
1622
1773
  const grantProfile = findProfileForConfiguredGrant();
1623
1774
  const connectionProfile = findProfileForConfiguredConnection(options);
1624
1775
  if (grantProfile && connectionProfile && grantProfile.id !== connectionProfile.id) {
1625
- throw new TaskforceRuleError('The authenticated OAuth grant and MCP connection are bound to different AI profiles. Select the intended profile in Taskforce before retrying.', 409, 'AI_PROFILE_BINDING_CONFLICT', { grantProfileId: grantProfile.id, connectionProfileId: connectionProfile.id });
1776
+ failAiProfileBindingConflict('The authenticated OAuth grant and MCP connection are bound to different AI profiles. Select the intended profile in Taskforce before retrying.', { reason: 'grant_connection_mismatch', grantProfileId: grantProfile.id, connectionProfileId: connectionProfile.id });
1626
1777
  }
1627
1778
  const profile = grantProfile || connectionProfile;
1628
1779
  if (profile && configuredMcpConnectionId && !connectionProfile && options?.repairConnection !== false) {
@@ -1635,6 +1786,51 @@ export function createTaskforceMcpServer(options = {}) {
1635
1786
  }
1636
1787
  return profile;
1637
1788
  }
1789
+ function assertOAuthProfileClientCompatibility(profile) {
1790
+ if (auditActorType !== 'oauth' || !configuredMcpClientId)
1791
+ return;
1792
+ const connection = profile.providerMetadata?.connection;
1793
+ const storedClientId = connection && typeof connection === 'object' && !Array.isArray(connection)
1794
+ ? parseMcpClientId(connection.clientId)
1795
+ : null;
1796
+ const inferredClientId = inferLegacyMcpClientIdFromProfile(profile);
1797
+ const profileClientId = storedClientId || inferredClientId;
1798
+ if (!profileClientId || profileClientId === configuredMcpClientId)
1799
+ return;
1800
+ throw new TaskforceRuleError('The supplied AI profile is not compatible with this OAuth client product.', 409, 'AI_PROFILE_SELECTION_INVALID', { profileId: profile.id, profileClientId, configuredClientId: configuredMcpClientId });
1801
+ }
1802
+ function listSelectableOAuthProfiles(profileName) {
1803
+ if (auditActorType !== 'oauth')
1804
+ return [];
1805
+ const requestedName = String(profileName || '').trim().toLowerCase();
1806
+ if (!requestedName)
1807
+ return [];
1808
+ const requestedUsername = normalizeConfiguredAiProfileUsername(undefined, requestedName).toLowerCase();
1809
+ const restorableProfileIds = !configuredMcpClientId && auditTokenId
1810
+ ? new Set(core.listMcpOAuthRestorableAiProfileIds({
1811
+ grantId: auditTokenId,
1812
+ workspaceId: ACTIVE_WORKSPACE_ID,
1813
+ }))
1814
+ : null;
1815
+ return core.listAiProfiles(ACTIVE_WORKSPACE_ID).filter((profile) => {
1816
+ if ((profile.seatScope ?? null) !== configuredAiProfileSeatScope)
1817
+ return false;
1818
+ const profileNameMatches = profile.name.trim().toLowerCase() === requestedName
1819
+ || profile.username.trim().toLowerCase() === requestedUsername;
1820
+ if (!profileNameMatches)
1821
+ return false;
1822
+ const connection = profile.providerMetadata?.connection;
1823
+ const storedClientId = connection && typeof connection === 'object' && !Array.isArray(connection)
1824
+ ? parseMcpClientId(connection.clientId)
1825
+ : null;
1826
+ const inferredClientId = inferLegacyMcpClientIdFromProfile(profile);
1827
+ if (!configuredMcpClientId)
1828
+ return restorableProfileIds?.has(profile.id) === true;
1829
+ return storedClientId
1830
+ ? storedClientId === configuredMcpClientId
1831
+ : inferredClientId === configuredMcpClientId;
1832
+ });
1833
+ }
1638
1834
  function getEligibleAssigneeActors() {
1639
1835
  return core.listAssignableActors(ACTIVE_WORKSPACE_ID)
1640
1836
  .filter((actor) => String(actor.id || '').trim().toLowerCase() !== 'agent')
@@ -1646,7 +1842,7 @@ export function createTaskforceMcpServer(options = {}) {
1646
1842
  .filter((actor) => Boolean(actor.id));
1647
1843
  }
1648
1844
  function describeEligibleAssignees() {
1649
- return "Use 'self' for the active AI profile, 'unassigned' to clear ownership, or an eligible ID from list_assignees.";
1845
+ return "Use 'self', 'unassigned', or an ID from list_assignees.";
1650
1846
  }
1651
1847
  function buildEligibleAssigneePayload() {
1652
1848
  return core.listAssignableActors(ACTIVE_WORKSPACE_ID)
@@ -2215,8 +2411,16 @@ export function createTaskforceMcpServer(options = {}) {
2215
2411
  function ensureResolvedProfileFromConnection(defaults) {
2216
2412
  if (activeAiProfileId)
2217
2413
  return;
2218
- const boundProfile = findTrustedBoundProfile();
2414
+ if (requiresExplicitTokenAuthorship)
2415
+ return;
2416
+ const boundProfile = findTrustedBoundProfile({ repairConnection: false });
2219
2417
  if (boundProfile) {
2418
+ if (configuredAiProfileIdentityAsserted) {
2419
+ assertRequestedIdentityMatchesBoundProfile(boundProfile, {
2420
+ name: configuredAiProfileName,
2421
+ provider: configuredAiProfileProvider,
2422
+ });
2423
+ }
2220
2424
  const configuredClientEntry = getMcpClientRegistryEntry(configuredMcpClientId);
2221
2425
  const resolved = core.resolveAiProfile({
2222
2426
  workspaceId: ACTIVE_WORKSPACE_ID,
@@ -2279,7 +2483,29 @@ export function createTaskforceMcpServer(options = {}) {
2279
2483
  return;
2280
2484
  if (!isMcpWriteToolName(name))
2281
2485
  return;
2486
+ if (aiProfileIdentityConflictDetected) {
2487
+ throw new TaskforceRuleError('Writes are blocked because this MCP session requested an identity that conflicts with its OAuth-bound profile. Reconnect with the intended profile and retry resolve_profile.', 409, 'AI_PROFILE_BINDING_CONFLICT', { reason: 'session_identity_quarantined', nextTool: 'resolve_profile' });
2488
+ }
2282
2489
  const profileToken = resolveProfileTokenFromArgs(args);
2490
+ if (requiresExplicitTokenAuthorship) {
2491
+ if (!profileToken) {
2492
+ throw new McpPublicError('Cloud access-token clients must pass profileToken on every mutating call. Call resolve_profile to obtain it, keep it private, and retry this write.', 'AI_PROFILE_PROOF_REQUIRED', 401);
2493
+ }
2494
+ const tokenProfile = core.getAiProfileByToken({
2495
+ workspaceId: ACTIVE_WORKSPACE_ID,
2496
+ profileToken,
2497
+ seatScope: configuredAiProfileSeatScope,
2498
+ });
2499
+ if (!tokenProfile) {
2500
+ throw new McpPublicError('Invalid AI profile token. Call resolve_profile again and retry.', 'INVALID_AI_PROFILE_TOKEN', 401);
2501
+ }
2502
+ if (activeAiProfileId && activeAiProfileId !== tokenProfile.id) {
2503
+ failAiProfileBindingConflict('The supplied profile token conflicts with the AI profile already active for this request.', { reason: 'request_token_mismatch', boundProfileId: activeAiProfileId, suppliedProfileId: tokenProfile.id });
2504
+ }
2505
+ if (!activeAiProfileId)
2506
+ activateAiProfile(tokenProfile.id, 'token_lookup');
2507
+ return;
2508
+ }
2283
2509
  if (activeAiProfileId) {
2284
2510
  if (!profileToken)
2285
2511
  return;
@@ -2292,7 +2518,7 @@ export function createTaskforceMcpServer(options = {}) {
2292
2518
  throw new McpPublicError('Invalid AI profile token. Call resolve_profile again and retry.', 'INVALID_AI_PROFILE_TOKEN', 401);
2293
2519
  }
2294
2520
  if (tokenProfile.id !== activeAiProfileId) {
2295
- throw new TaskforceRuleError('The supplied profile token conflicts with the AI profile already bound to this MCP session.', 409, 'AI_PROFILE_BINDING_CONFLICT', { boundProfileId: activeAiProfileId, suppliedProfileId: tokenProfile.id });
2521
+ failAiProfileBindingConflict('The supplied profile token conflicts with the AI profile already bound to this MCP session.', { reason: 'session_token_mismatch', boundProfileId: activeAiProfileId, suppliedProfileId: tokenProfile.id });
2296
2522
  }
2297
2523
  return;
2298
2524
  }
@@ -2306,6 +2532,22 @@ export function createTaskforceMcpServer(options = {}) {
2306
2532
  seatScope: configuredAiProfileSeatScope,
2307
2533
  });
2308
2534
  if (profile) {
2535
+ const trustedBoundProfile = findTrustedBoundProfile({ repairConnection: false });
2536
+ if (trustedBoundProfile && trustedBoundProfile.id !== profile.id) {
2537
+ failAiProfileBindingConflict('The supplied profile token conflicts with the profile already bound to this OAuth grant or MCP connection.', {
2538
+ reason: 'stateless_token_mismatch',
2539
+ boundProfileId: trustedBoundProfile.id,
2540
+ suppliedProfileId: profile.id,
2541
+ });
2542
+ }
2543
+ if (!trustedBoundProfile)
2544
+ assertOAuthProfileClientCompatibility(profile);
2545
+ if (trustedBoundProfile && configuredAiProfileIdentityAsserted) {
2546
+ assertRequestedIdentityMatchesBoundProfile(trustedBoundProfile, {
2547
+ name: configuredAiProfileName,
2548
+ provider: configuredAiProfileProvider,
2549
+ });
2550
+ }
2309
2551
  activateAiProfile(profile.id, 'token_lookup');
2310
2552
  return;
2311
2553
  }
@@ -2334,7 +2576,7 @@ export function createTaskforceMcpServer(options = {}) {
2334
2576
  throw new McpPublicError('This tool requires an AI profile. Call resolve_profile first, then retry. If your MCP client is stateless, pass profileToken on mutating calls.', 'AI_PROFILE_REQUIRED');
2335
2577
  }
2336
2578
  function assertAnnotatedAttachmentReadAccess() {
2337
- const gate = resolveAppGateAccess(ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY);
2579
+ const gate = resolveNodeAppGateAccess(ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY);
2338
2580
  if (!gate.allowed) {
2339
2581
  throw new TaskforceRuleError('Annotated attachments are unavailable for this workspace.', 403, gate.code || 'ANNOTATED_ATTACHMENTS_UNAVAILABLE');
2340
2582
  }
@@ -3049,15 +3291,11 @@ export function createTaskforceMcpServer(options = {}) {
3049
3291
  || planningSearchIncludes(initiative?.referenceLabel || formatInitiativeReference(initiative?.referenceNumber), query)
3050
3292
  || planningSearchIncludes(initiative?.title, query);
3051
3293
  }
3052
- function buildHistoricalWorkspaceTaskSnapshot() {
3053
- const activeTasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks;
3054
- const archivedTasks = core.listArchive(ACTIVE_WORKSPACE_ID).archived;
3055
- const byId = new Map();
3056
- for (const task of activeTasks)
3057
- byId.set(task.id, task);
3058
- for (const task of archivedTasks)
3059
- byId.set(task.id, task);
3060
- const tasks = Array.from(byId.values());
3294
+ function buildHistoricalWorkspaceTaskSnapshot(workstreamId) {
3295
+ const tasks = core.listTasksSlim(ACTIVE_WORKSPACE_ID, {
3296
+ ...(workstreamId ? { workstreamId } : {}),
3297
+ includeArchived: true,
3298
+ });
3061
3299
  const tasksByWorkstreamId = new Map();
3062
3300
  for (const task of tasks) {
3063
3301
  const normalizedWorkstreamId = String(task.workstreamId || '').trim();
@@ -3090,6 +3328,34 @@ export function createTaskforceMcpServer(options = {}) {
3090
3328
  semantics: 'historical: active + archived',
3091
3329
  };
3092
3330
  }
3331
+ function emptyPlanningTaskCounts() {
3332
+ return buildPlanningTaskCounts([]);
3333
+ }
3334
+ function buildHistoricalTaskCountSnapshot(workstreamId) {
3335
+ return new Map(core.listTaskCountsByWorkstream(ACTIVE_WORKSPACE_ID, workstreamId).map((entry) => [
3336
+ entry.workstreamId,
3337
+ {
3338
+ total: entry.total,
3339
+ active: entry.active,
3340
+ archived: entry.archived,
3341
+ semantics: 'historical: active + archived',
3342
+ },
3343
+ ]));
3344
+ }
3345
+ function sumPlanningTaskCounts(workstreams, countsByWorkstreamId) {
3346
+ let total = 0;
3347
+ let active = 0;
3348
+ let archived = 0;
3349
+ for (const workstream of workstreams) {
3350
+ const counts = countsByWorkstreamId.get(workstream.id);
3351
+ if (!counts)
3352
+ continue;
3353
+ total += counts.total;
3354
+ active += counts.active;
3355
+ archived += counts.archived;
3356
+ }
3357
+ return { total, active, archived, semantics: 'historical: active + archived' };
3358
+ }
3093
3359
  function buildPlanningWorkstreamCounts(workstreams) {
3094
3360
  const active = workstreams.filter((workstream) => !workstream?.isArchived).length;
3095
3361
  const archived = workstreams.length - active;
@@ -3125,6 +3391,17 @@ export function createTaskforceMcpServer(options = {}) {
3125
3391
  offset: Number(normalizedOffset),
3126
3392
  };
3127
3393
  }
3394
+ function normalizeSearchReadPage(limit, offset, defaultLimit) {
3395
+ const normalizedLimit = limit === undefined ? defaultLimit : limit;
3396
+ const normalizedOffset = offset === undefined ? 0 : offset;
3397
+ if (!Number.isInteger(normalizedLimit) || Number(normalizedLimit) < 1 || Number(normalizedLimit) > 50) {
3398
+ throw new McpPublicError('limit must be an integer from 1 to 50');
3399
+ }
3400
+ if (!Number.isInteger(normalizedOffset) || Number(normalizedOffset) < 0) {
3401
+ throw new McpPublicError('offset must be a non-negative integer');
3402
+ }
3403
+ return { limit: Number(normalizedLimit), offset: Number(normalizedOffset) };
3404
+ }
3128
3405
  function buildCompactPlanningHistory(targetType, entity) {
3129
3406
  const duplicatedCommentAction = `${targetType}-comment-added`;
3130
3407
  const activity = Array.isArray(entity?.activity) ? entity.activity : [];
@@ -3192,16 +3469,19 @@ export function createTaskforceMcpServer(options = {}) {
3192
3469
  };
3193
3470
  }
3194
3471
  function buildInitiativeResourcePayload(initiative, options) {
3195
- const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
3472
+ if (options?.detail === 'history') {
3473
+ return buildPlanningHistoryPayload('initiative', initiative, options.offset ?? 0, options.limit ?? 25);
3474
+ }
3196
3475
  const workstreams = options?.workstreamsByInitiativeId?.get(initiative.id)
3197
3476
  || core.listWorkstreams(ACTIVE_WORKSPACE_ID)
3198
3477
  .filter((workstream) => workstream.initiativeId === initiative.id);
3199
- const tasks = workstreams.flatMap((workstream) => tasksByWorkstreamId.get(workstream.id) || []);
3478
+ const taskCountsByWorkstreamId = options?.taskCountsByWorkstreamId || buildHistoricalTaskCountSnapshot();
3479
+ const taskCounts = sumPlanningTaskCounts(workstreams, taskCountsByWorkstreamId);
3200
3480
  if (options?.detail === 'compact') {
3201
3481
  return {
3202
3482
  initiative: buildPlanningInitiativeSummary(initiative),
3203
3483
  workstreamCount: workstreams.length,
3204
- taskCount: tasks.length,
3484
+ taskCount: taskCounts.total,
3205
3485
  nextDetailOption: 'full',
3206
3486
  };
3207
3487
  }
@@ -3215,13 +3495,12 @@ export function createTaskforceMcpServer(options = {}) {
3215
3495
  initiative: withPlanningEntityUrl('initiative', buildPlanningEntityMetadata('initiative', initiative)),
3216
3496
  attachments: listPlanningAttachmentDescriptors({ id: initiative.id, entity: initiative }),
3217
3497
  workstreamCounts: buildPlanningWorkstreamCounts(workstreams),
3218
- taskCounts: buildPlanningTaskCounts(tasks),
3498
+ taskCounts,
3219
3499
  workstreams: page.map((workstream) => {
3220
- const workstreamTasks = tasksByWorkstreamId.get(workstream.id) || [];
3221
3500
  return {
3222
3501
  workstream: buildPlanningWorkstreamSummary(workstream),
3223
3502
  attachments: listPlanningAttachmentDescriptors({ id: workstream.id, entity: workstream }),
3224
- taskCounts: buildPlanningTaskCounts(workstreamTasks),
3503
+ taskCounts: taskCountsByWorkstreamId.get(workstream.id) || emptyPlanningTaskCounts(),
3225
3504
  };
3226
3505
  }),
3227
3506
  pagination,
@@ -3237,27 +3516,38 @@ export function createTaskforceMcpServer(options = {}) {
3237
3516
  } : { truncated: false }),
3238
3517
  };
3239
3518
  }
3240
- if (options?.detail === 'history') {
3241
- return buildPlanningHistoryPayload('initiative', initiative, options.offset ?? 0, options.limit ?? 25);
3242
- }
3243
3519
  return {
3244
3520
  initiative: buildPlanningInitiativeDetail(initiative),
3245
3521
  workstreamCount: workstreams.length,
3246
- taskCount: tasks.length,
3522
+ taskCount: taskCounts.total,
3247
3523
  workstreams: workstreams.map((workstream) => buildPlanningWorkstreamSummary(workstream)),
3248
3524
  };
3249
3525
  }
3250
3526
  function buildWorkstreamResourcePayload(workstream, options) {
3251
- const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
3252
- const tasks = tasksByWorkstreamId.get(workstream.id)
3253
- || [];
3527
+ if (options?.detail === 'history') {
3528
+ return buildPlanningHistoryPayload('workstream', workstream, options.offset ?? 0, options.limit ?? 25);
3529
+ }
3254
3530
  const workstreamReference = typeof workstream.referenceLabel === 'string'
3255
3531
  ? workstream.referenceLabel
3256
3532
  : formatWorkstreamReference(workstream);
3533
+ if (options?.detail !== 'compact' && options?.detail !== 'planning') {
3534
+ const tasksByWorkstreamId = options?.tasksByWorkstreamId
3535
+ || buildHistoricalWorkspaceTaskSnapshot(workstream.id).tasksByWorkstreamId;
3536
+ const tasks = tasksByWorkstreamId.get(workstream.id) || [];
3537
+ return {
3538
+ workstream: buildPlanningWorkstreamDetail(workstream),
3539
+ taskCount: tasks.length,
3540
+ tasks,
3541
+ nextTaskTool: 'list_tasks',
3542
+ suggestedTaskArgs: { workstreamId: workstreamReference, format: 'json', limit: 50, offset: 0 },
3543
+ };
3544
+ }
3545
+ const taskCounts = (options?.taskCountsByWorkstreamId || buildHistoricalTaskCountSnapshot(workstream.id))
3546
+ .get(workstream.id) || emptyPlanningTaskCounts();
3257
3547
  if (options?.detail === 'compact') {
3258
3548
  return {
3259
3549
  workstream: buildPlanningWorkstreamSummary(workstream),
3260
- taskCount: tasks.length,
3550
+ taskCount: taskCounts.total,
3261
3551
  nextDetailOption: 'full',
3262
3552
  nextTaskTool: 'list_tasks',
3263
3553
  suggestedTaskArgs: { workstreamId: workstreamReference, format: 'json', limit: 50, offset: 0 },
@@ -3267,22 +3557,13 @@ export function createTaskforceMcpServer(options = {}) {
3267
3557
  return {
3268
3558
  workstream: withPlanningEntityUrl('workstream', buildPlanningEntityMetadata('workstream', workstream)),
3269
3559
  attachments: listPlanningAttachmentDescriptors({ id: workstream.id, entity: workstream }),
3270
- taskCounts: buildPlanningTaskCounts(tasks),
3560
+ taskCounts,
3271
3561
  nextTaskTool: 'list_tasks',
3272
3562
  suggestedTaskArgs: { workstreamId: workstreamReference, format: 'json', limit: 50, offset: 0 },
3273
3563
  guidance: 'taskCounts are historical (active + archived). list_tasks returns bounded active task rows; use search_tasks with scope=archive for archived task discovery.',
3274
3564
  };
3275
3565
  }
3276
- if (options?.detail === 'history') {
3277
- return buildPlanningHistoryPayload('workstream', workstream, options.offset ?? 0, options.limit ?? 25);
3278
- }
3279
- return {
3280
- workstream: buildPlanningWorkstreamDetail(workstream),
3281
- taskCount: tasks.length,
3282
- tasks,
3283
- nextTaskTool: 'list_tasks',
3284
- suggestedTaskArgs: { workstreamId: workstreamReference, format: 'json', limit: 50, offset: 0 },
3285
- };
3566
+ throw new Error(`Unhandled workstream detail: ${options?.detail}`);
3286
3567
  }
3287
3568
  function buildInitiativesResourcePayload(options) {
3288
3569
  const query = normalizePlanningSearchQuery(options?.query);
@@ -3296,18 +3577,27 @@ export function createTaskforceMcpServer(options = {}) {
3296
3577
  current.push(workstream);
3297
3578
  workstreamsByInitiativeId.set(normalizedInitiativeId, current);
3298
3579
  }
3299
- const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
3300
- const initiatives = core.listInitiatives(ACTIVE_WORKSPACE_ID)
3301
- .filter((initiative) => matchesInitiativePlanningQuery(initiative, query, workstreamsByInitiativeId.get(initiative.id) || []))
3302
- .map((initiative) => buildInitiativeResourcePayload(initiative, {
3303
- workstreamsByInitiativeId,
3304
- tasksByWorkstreamId,
3305
- detail: 'compact',
3306
- }));
3580
+ const taskCountsByWorkstreamId = buildHistoricalTaskCountSnapshot();
3581
+ const matches = core.listInitiatives(ACTIVE_WORKSPACE_ID)
3582
+ .filter((initiative) => matchesInitiativePlanningQuery(initiative, query, workstreamsByInitiativeId.get(initiative.id) || []));
3583
+ const offset = options?.offset ?? 0;
3584
+ const limit = options?.limit ?? 25;
3585
+ const initiatives = matches.slice(offset, offset + limit).map((initiative) => {
3586
+ const compact = buildInitiativeResourcePayload(initiative, {
3587
+ workstreamsByInitiativeId,
3588
+ taskCountsByWorkstreamId,
3589
+ detail: 'compact',
3590
+ });
3591
+ const { nextDetailOption: _nextDetailOption, ...summary } = compact;
3592
+ return summary;
3593
+ });
3594
+ const pagination = buildPlanningPagination(matches.length, offset, limit, initiatives.length);
3307
3595
  return {
3308
- total: initiatives.length,
3596
+ total: matches.length,
3309
3597
  ...(query ? { query } : {}),
3310
3598
  initiatives,
3599
+ pagination,
3600
+ guidance: 'Use get_initiative with the initiative ID or reference for details.',
3311
3601
  };
3312
3602
  }
3313
3603
  function buildWorkstreamsResourcePayload(options) {
@@ -3316,14 +3606,23 @@ export function createTaskforceMcpServer(options = {}) {
3316
3606
  for (const initiative of core.listInitiatives(ACTIVE_WORKSPACE_ID)) {
3317
3607
  initiativesById.set(initiative.id, initiative);
3318
3608
  }
3319
- const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
3320
- const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID)
3321
- .filter((workstream) => matchesWorkstreamPlanningQuery(workstream, query, initiativesById.get(workstream.initiativeId || '')))
3322
- .map((workstream) => buildWorkstreamResourcePayload(workstream, { tasksByWorkstreamId, detail: 'compact' }));
3609
+ const taskCountsByWorkstreamId = buildHistoricalTaskCountSnapshot();
3610
+ const matches = core.listWorkstreams(ACTIVE_WORKSPACE_ID)
3611
+ .filter((workstream) => matchesWorkstreamPlanningQuery(workstream, query, initiativesById.get(workstream.initiativeId || '')));
3612
+ const offset = options?.offset ?? 0;
3613
+ const limit = options?.limit ?? 25;
3614
+ const workstreams = matches.slice(offset, offset + limit).map((workstream) => {
3615
+ const compact = buildWorkstreamResourcePayload(workstream, { taskCountsByWorkstreamId, detail: 'compact' });
3616
+ const { nextDetailOption: _nextDetailOption, nextTaskTool: _nextTaskTool, suggestedTaskArgs: _suggestedTaskArgs, ...summary } = compact;
3617
+ return summary;
3618
+ });
3619
+ const pagination = buildPlanningPagination(matches.length, offset, limit, workstreams.length);
3323
3620
  return {
3324
- total: workstreams.length,
3621
+ total: matches.length,
3325
3622
  ...(query ? { query } : {}),
3326
3623
  workstreams,
3624
+ pagination,
3625
+ guidance: 'Use get_workstream for details or list_tasks with workstreamId for active tasks.',
3327
3626
  };
3328
3627
  }
3329
3628
  function buildPlanningWriteEntitySummary(targetType, entity) {
@@ -3624,13 +3923,16 @@ export function createTaskforceMcpServer(options = {}) {
3624
3923
  registerAdminWorkspaceTools(registerTool, (name, args) => executeRegisteredTool(name, args));
3625
3924
  const mutatingProfileTokenProperty = {
3626
3925
  type: 'string',
3627
- description: 'Optional profileToken from resolve_profile for stateless MCP clients.'
3926
+ description: requiresExplicitTokenAuthorship
3927
+ ? 'Required authorship proof for cloud access-token clients; obtain from resolve_profile.'
3928
+ : 'Optional profileToken from resolve_profile for stateless MCP clients.'
3628
3929
  };
3629
3930
  const nonDestructiveWriteTools = new Set([
3630
3931
  'add_annotated_attachment_marker',
3631
3932
  'add_comment',
3632
3933
  'add_document_review_comment',
3633
3934
  'add_initiative_comment',
3935
+ 'add_task_checklist_item',
3634
3936
  'add_workstream_comment',
3635
3937
  'bulk_add_annotated_attachment_markers',
3636
3938
  'create_annotated_attachment_session',
@@ -3639,6 +3941,7 @@ export function createTaskforceMcpServer(options = {}) {
3639
3941
  'create_task',
3640
3942
  'create_workstream',
3641
3943
  'link_document',
3944
+ 'link_task_relationship',
3642
3945
  'rehome_document',
3643
3946
  'link_task_context',
3644
3947
  'resolve_profile',
@@ -3655,6 +3958,9 @@ export function createTaskforceMcpServer(options = {}) {
3655
3958
  get priorityDesc() {
3656
3959
  return `Priority level: ${getActivePriorities().map((priority) => `${priority.value} (${priority.label})`).join(', ')}`;
3657
3960
  },
3961
+ get priorityEnum() {
3962
+ return getActivePriorities().map((priority) => priority.value);
3963
+ },
3658
3964
  get typeEnum() {
3659
3965
  return getActiveTypes().map((type) => type.value);
3660
3966
  },
@@ -3662,7 +3968,7 @@ export function createTaskforceMcpServer(options = {}) {
3662
3968
  get assigneeHint() {
3663
3969
  return describeEligibleAssignees();
3664
3970
  },
3665
- describeTool: (text) => `[Project: ${PROJECT_NAME}] ${text}`,
3971
+ describeTool: (text) => text,
3666
3972
  };
3667
3973
  };
3668
3974
  const titleForTool = (name) => String(name || '')
@@ -3695,6 +4001,11 @@ export function createTaskforceMcpServer(options = {}) {
3695
4001
  inputSchema: {
3696
4002
  ...descriptor.inputSchema,
3697
4003
  properties,
4004
+ ...(requiresExplicitTokenAuthorship
4005
+ && descriptor.name !== 'resolve_profile'
4006
+ && isMcpWriteToolName(descriptor.name)
4007
+ ? { required: Array.from(new Set([...(descriptor.inputSchema.required || []), 'profileToken'])) }
4008
+ : {}),
3698
4009
  additionalProperties: false,
3699
4010
  },
3700
4011
  annotations: {
@@ -4529,6 +4840,7 @@ export function createTaskforceMcpServer(options = {}) {
4529
4840
  uploadDraftId: result.uploadDraftId,
4530
4841
  mimeType: result.mimeType,
4531
4842
  uploadedPath: result.uploadedPath,
4843
+ ...(result.displayName ? { displayName: result.displayName } : {}),
4532
4844
  ...(result.caption ? { caption: result.caption } : {}),
4533
4845
  ...(result.overwrite ? { overwrite: true } : {}),
4534
4846
  ...(result.expectedSizeBytes !== undefined ? { expectedSizeBytes: result.expectedSizeBytes } : {}),
@@ -4770,15 +5082,15 @@ export function createTaskforceMcpServer(options = {}) {
4770
5082
  : resolveWorkstreamByIdentifierOrThrow(workstreamId).id;
4771
5083
  const paginated = resolvedWorkstreamId !== undefined || offset !== undefined;
4772
5084
  const parsedOffset = offset === undefined ? 0 : Number(offset);
4773
- if (!Number.isFinite(parsedOffset) || parsedOffset < 0)
4774
- throw new McpPublicError('offset must be a non-negative number');
4775
- const normalizedOffset = Math.floor(parsedOffset);
5085
+ if (!Number.isInteger(parsedOffset) || parsedOffset < 0)
5086
+ throw new McpPublicError('offset must be a non-negative integer');
5087
+ const normalizedOffset = parsedOffset;
4776
5088
  const parsedLimit = limit === undefined ? (paginated ? 50 : 0) : Number(limit);
4777
- if (!Number.isFinite(parsedLimit) || parsedLimit < 0)
4778
- throw new McpPublicError('limit must be a non-negative number');
5089
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 0 || parsedLimit > 200)
5090
+ throw new McpPublicError('limit must be an integer from 1 to 200');
4779
5091
  const normalizedLimit = paginated
4780
- ? Math.min(200, Math.max(1, Math.floor(parsedLimit || 50)))
4781
- : Math.floor(parsedLimit);
5092
+ ? (parsedLimit || 50)
5093
+ : parsedLimit;
4782
5094
  const queriedTasks = core.listTasksSlim(ACTIVE_WORKSPACE_ID, {
4783
5095
  category,
4784
5096
  status,
@@ -4827,8 +5139,12 @@ export function createTaskforceMcpServer(options = {}) {
4827
5139
  };
4828
5140
  }
4829
5141
  case "resolve_profile": {
4830
- const { name: profileName, icon, color, description, role, provider, model, surfaceType, profileToken } = args;
5142
+ const { name: profileName, icon, color, description, role, provider, model, surfaceType, profileId, profileToken } = args;
5143
+ if (aiProfileIdentityConflictDetected) {
5144
+ throw new TaskforceRuleError('This MCP session is quarantined after a profile identity conflict. Reconnect with the intended profile before retrying.', 409, 'AI_PROFILE_BINDING_CONFLICT', { reason: 'session_identity_quarantined', nextTool: 'resolve_profile' });
5145
+ }
4831
5146
  const suppliedProfileToken = typeof profileToken === 'string' ? profileToken.trim() : '';
5147
+ const selectedProfileId = typeof profileId === 'string' ? profileId.trim() : '';
4832
5148
  const sessionBoundProfile = activeAiProfileId
4833
5149
  ? core.listAiProfiles(ACTIVE_WORKSPACE_ID, { includeArchived: true })
4834
5150
  .find((profile) => profile.id === activeAiProfileId) || null
@@ -4841,9 +5157,35 @@ export function createTaskforceMcpServer(options = {}) {
4841
5157
  repairConnection: false,
4842
5158
  });
4843
5159
  if (sessionBoundProfile && trustedBoundProfile && sessionBoundProfile.id !== trustedBoundProfile.id) {
4844
- throw new TaskforceRuleError('The MCP session and authenticated connection are bound to different AI profiles. Reconnect before retrying.', 409, 'AI_PROFILE_BINDING_CONFLICT', { sessionProfileId: sessionBoundProfile.id, trustedProfileId: trustedBoundProfile.id });
5160
+ failAiProfileBindingConflict('The MCP session and authenticated connection are bound to different AI profiles. Reconnect before retrying.', { reason: 'session_connection_mismatch', sessionProfileId: sessionBoundProfile.id, trustedProfileId: trustedBoundProfile.id });
5161
+ }
5162
+ const selectableProfiles = listSelectableOAuthProfiles(profileName);
5163
+ const explicitlySelectedProfile = selectedProfileId
5164
+ ? selectableProfiles.find((profile) => profile.id === selectedProfileId) || null
5165
+ : null;
5166
+ if (selectedProfileId && !explicitlySelectedProfile) {
5167
+ throw new TaskforceRuleError('The selected AI profile is not an active compatible identity for this OAuth client and requested name.', 409, 'AI_PROFILE_SELECTION_INVALID', { selectedProfileId });
5168
+ }
5169
+ const alreadyBoundProfile = sessionBoundProfile || trustedBoundProfile;
5170
+ if (alreadyBoundProfile) {
5171
+ assertRequestedIdentityMatchesBoundProfile(alreadyBoundProfile, {
5172
+ name: profileName,
5173
+ provider,
5174
+ });
5175
+ }
5176
+ if (alreadyBoundProfile && explicitlySelectedProfile && alreadyBoundProfile.id !== explicitlySelectedProfile.id) {
5177
+ failAiProfileBindingConflict('The selected AI profile conflicts with the profile already bound to this MCP session or authenticated connection.', { reason: 'selected_profile_mismatch', boundProfileId: alreadyBoundProfile.id, selectedProfileId: explicitlySelectedProfile.id });
5178
+ }
5179
+ const boundProfile = alreadyBoundProfile || explicitlySelectedProfile;
5180
+ if (!boundProfile && suppliedProfileToken) {
5181
+ const suppliedProfile = core.getAiProfileByToken({
5182
+ workspaceId: ACTIVE_WORKSPACE_ID,
5183
+ profileToken: suppliedProfileToken,
5184
+ seatScope: configuredAiProfileSeatScope,
5185
+ });
5186
+ if (suppliedProfile)
5187
+ assertOAuthProfileClientCompatibility(suppliedProfile);
4845
5188
  }
4846
- const boundProfile = sessionBoundProfile || trustedBoundProfile;
4847
5189
  if (boundProfile && suppliedProfileToken) {
4848
5190
  const tokenProfile = core.getAiProfileByToken({
4849
5191
  workspaceId: ACTIVE_WORKSPACE_ID,
@@ -4851,7 +5193,7 @@ export function createTaskforceMcpServer(options = {}) {
4851
5193
  seatScope: configuredAiProfileSeatScope,
4852
5194
  });
4853
5195
  if (!tokenProfile || tokenProfile.id !== boundProfile.id) {
4854
- throw new TaskforceRuleError('The supplied profile token conflicts with the AI profile already bound to this MCP session or authenticated connection.', 409, 'AI_PROFILE_BINDING_CONFLICT', { boundProfileId: boundProfile.id, ...(tokenProfile ? { suppliedProfileId: tokenProfile.id } : {}) });
5196
+ failAiProfileBindingConflict('The supplied profile token conflicts with the AI profile already bound to this MCP session or authenticated connection.', { reason: 'bound_token_mismatch', boundProfileId: boundProfile.id, ...(tokenProfile ? { suppliedProfileId: tokenProfile.id } : {}) });
4855
5197
  }
4856
5198
  }
4857
5199
  if (!boundProfile && !suppliedProfileToken && auditActorType === 'oauth') {
@@ -4860,7 +5202,16 @@ export function createTaskforceMcpServer(options = {}) {
4860
5202
  const hasIdentityCandidate = core.listAiProfiles(ACTIVE_WORKSPACE_ID).some((profile) => ((profile.seatScope ?? null) === configuredAiProfileSeatScope
4861
5203
  && (profile.name.trim().toLowerCase() === requestedName || profile.username.trim().toLowerCase() === requestedUsername)));
4862
5204
  if (hasIdentityCandidate) {
4863
- throw new TaskforceRuleError('An AI profile with this identity already exists, but it is not bound to this OAuth client. Select the intended profile in Taskforce before retrying.', 409, 'AI_PROFILE_SELECTION_REQUIRED');
5205
+ const candidates = selectableProfiles.map((profile) => buildAiProfileDescriptor(profile));
5206
+ const suggestedArgs = candidates.length === 1
5207
+ ? { name: profileName, profileId: candidates[0].id }
5208
+ : undefined;
5209
+ throw new TaskforceRuleError(candidates.length > 0
5210
+ ? 'An AI profile with this identity already exists but is not bound to this OAuth client. Select a compatible candidate explicitly and retry resolve_profile.'
5211
+ : 'An AI profile with this identity already exists but is not compatible with this OAuth client. Select the intended profile in Taskforce settings before retrying.', 409, 'AI_PROFILE_SELECTION_REQUIRED', {
5212
+ candidates,
5213
+ ...(suggestedArgs ? { suggestedArgs } : {}),
5214
+ });
4864
5215
  }
4865
5216
  }
4866
5217
  const configuredClientEntry = getMcpClientRegistryEntry(configuredMcpClientId);
@@ -5039,10 +5390,19 @@ export function createTaskforceMcpServer(options = {}) {
5039
5390
  };
5040
5391
  }
5041
5392
  case "search_tasks": {
5042
- const { query, scope = "all" } = args;
5043
- const results = core.searchTasks(query, scope, ACTIVE_WORKSPACE_ID);
5393
+ const { query, scope = "all", limit, offset } = args;
5394
+ const page = normalizeSearchReadPage(limit, offset, 25);
5395
+ const queried = core.searchTaskSummaries(query, scope, ACTIVE_WORKSPACE_ID, {
5396
+ limit: page.limit + 1,
5397
+ offset: page.offset,
5398
+ });
5399
+ const hasMore = queried.length > page.limit;
5400
+ const results = hasMore ? queried.slice(0, page.limit) : queried;
5044
5401
  return {
5045
- content: [{ type: "text", text: renderTaskTable(results) }],
5402
+ content: [{
5403
+ type: "text",
5404
+ text: `${renderTaskTable(results)}\n\nPage: offset ${page.offset}, limit ${page.limit}, returned ${results.length}, hasMore ${hasMore}${hasMore ? `, nextOffset ${page.offset + results.length}` : ''}`,
5405
+ }],
5046
5406
  };
5047
5407
  }
5048
5408
  case "get_task": {
@@ -5200,9 +5560,11 @@ export function createTaskforceMcpServer(options = {}) {
5200
5560
  case "list_task_relationships": {
5201
5561
  const { taskId, type, direction = 'both' } = args;
5202
5562
  const { id } = resolveTaskByIdentifierOrThrow(taskId);
5203
- const relationships = core.listTaskRelationships(id, ACTIVE_WORKSPACE_ID, { type, direction }).map((relationship) => {
5204
- const source = core.getTask(relationship.sourceTaskId, ACTIVE_WORKSPACE_ID);
5205
- const target = core.getTask(relationship.targetTaskId, ACTIVE_WORKSPACE_ID);
5563
+ const relationshipRows = core.listTaskRelationships(id, ACTIVE_WORKSPACE_ID, { type, direction });
5564
+ const taskSummariesById = new Map(core.listTaskReferenceSummaries(relationshipRows.flatMap((relationship) => [relationship.sourceTaskId, relationship.targetTaskId]), ACTIVE_WORKSPACE_ID).map((task) => [task.id, task]));
5565
+ const relationships = relationshipRows.map((relationship) => {
5566
+ const source = taskSummariesById.get(relationship.sourceTaskId) || null;
5567
+ const target = taskSummariesById.get(relationship.targetTaskId) || null;
5206
5568
  const perspectiveType = relationship.kind === 'dependency'
5207
5569
  ? (relationship.sourceTaskId === id ? 'depends_on' : 'blocks')
5208
5570
  : relationship.kind === 'related' ? 'related_to' : 'supersedes';
@@ -5244,9 +5606,10 @@ export function createTaskforceMcpServer(options = {}) {
5244
5606
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
5245
5607
  }
5246
5608
  case "list_initiatives": {
5247
- const { query } = args;
5609
+ const { query, limit, offset } = args;
5610
+ const page = normalizePlanningReadPage(limit, offset);
5248
5611
  return {
5249
- content: [{ type: "text", text: JSON.stringify(buildInitiativesResourcePayload({ query }), null, 2) }],
5612
+ content: [{ type: "text", text: JSON.stringify(buildInitiativesResourcePayload({ query, ...page }), null, 2) }],
5250
5613
  };
5251
5614
  }
5252
5615
  case "get_initiative": {
@@ -5367,12 +5730,21 @@ export function createTaskforceMcpServer(options = {}) {
5367
5730
  return { content: [{ type: 'text', text: JSON.stringify(linkedResult, null, 2) }] };
5368
5731
  }
5369
5732
  case "create_initiative": {
5370
- const router = getDurableInitiativeMutationRouter();
5733
+ const { idempotencyKey: rawIdempotencyKey, ...initiativeArgs } = args;
5734
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
5735
+ const operationId = idempotencyKey
5736
+ ? stableExplicitMutationId('operation', 'create_initiative', idempotencyKey)
5737
+ : undefined;
5738
+ const router = getDurableInitiativeMutationRouter(operationId ? { operationId } : undefined);
5739
+ if (idempotencyKey)
5740
+ requireDurableExplicitIdempotency(router.enabled, 'initiative');
5371
5741
  let prepared;
5372
5742
  try {
5373
- prepared = planningCommandPreparationService.prepareInitiativeCreate(args, {
5743
+ prepared = planningCommandPreparationService.prepareInitiativeCreate(initiativeArgs, {
5374
5744
  durable: router.enabled,
5375
- entityId: requireStableMcpRequestId('initiative', 'create'),
5745
+ entityId: idempotencyKey
5746
+ ? stableExplicitMutationId('initiative', 'create_initiative', idempotencyKey)
5747
+ : requireStableMcpRequestId('initiative', 'create'),
5376
5748
  createdAt: new Date().toISOString(),
5377
5749
  });
5378
5750
  }
@@ -5421,14 +5793,23 @@ export function createTaskforceMcpServer(options = {}) {
5421
5793
  };
5422
5794
  }
5423
5795
  case "add_initiative_comment": {
5424
- const { id, text } = args;
5796
+ const { id, text, idempotencyKey: rawIdempotencyKey } = args;
5425
5797
  const { id: initiativeId } = resolveInitiativeByIdentifierOrThrow(id);
5426
5798
  const validation = validateStructuredCommentForAdd(text);
5427
5799
  if (!validation.ok) {
5428
5800
  throw new McpPublicError(validation.message);
5429
5801
  }
5430
5802
  const effectiveAuthor = getActiveMcpActorRef();
5431
- const router = getDurableInitiativeMutationRouter();
5803
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
5804
+ const explicitCommentId = idempotencyKey
5805
+ ? stableExplicitMutationId('comment', 'add_initiative_comment', idempotencyKey)
5806
+ : null;
5807
+ const router = getDurableInitiativeMutationRouter(idempotencyKey ? {
5808
+ operationId: stableExplicitMutationId('operation', 'add_initiative_comment', idempotencyKey),
5809
+ commentId: explicitCommentId,
5810
+ } : undefined);
5811
+ if (idempotencyKey)
5812
+ requireDurableExplicitIdempotency(router.enabled, 'initiative comment');
5432
5813
  const updated = router.enabled
5433
5814
  ? initiativeFromDurableResult(router.appendComment(initiativeId, {
5434
5815
  author: effectiveAuthor,
@@ -5440,7 +5821,11 @@ export function createTaskforceMcpServer(options = {}) {
5440
5821
  if (!updated) {
5441
5822
  throw new McpPublicError(buildInitiativeLookupFailureMessage(String(id || '').trim()), 'MCP_NOT_FOUND', 404);
5442
5823
  }
5443
- const persistedComment = Array.isArray(updated.comments) ? updated.comments.at(-1) : null;
5824
+ const persistedComment = Array.isArray(updated.comments)
5825
+ ? (explicitCommentId
5826
+ ? updated.comments.find((candidate) => candidate.id === explicitCommentId) || null
5827
+ : updated.comments.at(-1) || null)
5828
+ : null;
5444
5829
  recordMcpWriteAudit('add_initiative_comment', {
5445
5830
  entityType: 'initiative',
5446
5831
  initiativeId: updated.id,
@@ -5454,7 +5839,7 @@ export function createTaskforceMcpServer(options = {}) {
5454
5839
  return {
5455
5840
  content: [{ type: "text", text: JSON.stringify(buildPlanningWriteAcknowledgement('initiative', updated, 'add_initiative_comment', {
5456
5841
  changedFields: ['comments'],
5457
- comment: Array.isArray(updated.comments) ? updated.comments.at(-1) : null,
5842
+ comment: persistedComment,
5458
5843
  }), null, 2) }],
5459
5844
  };
5460
5845
  }
@@ -5495,9 +5880,10 @@ export function createTaskforceMcpServer(options = {}) {
5495
5880
  };
5496
5881
  }
5497
5882
  case "list_workstreams": {
5498
- const { query } = args;
5883
+ const { query, limit, offset } = args;
5884
+ const page = normalizePlanningReadPage(limit, offset);
5499
5885
  return {
5500
- content: [{ type: "text", text: JSON.stringify(buildWorkstreamsResourcePayload({ query }), null, 2) }],
5886
+ content: [{ type: "text", text: JSON.stringify(buildWorkstreamsResourcePayload({ query, ...page }), null, 2) }],
5501
5887
  };
5502
5888
  }
5503
5889
  case "get_workstream": {
@@ -5515,12 +5901,21 @@ export function createTaskforceMcpServer(options = {}) {
5515
5901
  };
5516
5902
  }
5517
5903
  case "create_workstream": {
5518
- const router = getDurableWorkstreamMutationRouter();
5904
+ const { idempotencyKey: rawIdempotencyKey, ...workstreamArgs } = args;
5905
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
5906
+ const operationId = idempotencyKey
5907
+ ? stableExplicitMutationId('operation', 'create_workstream', idempotencyKey)
5908
+ : undefined;
5909
+ const router = getDurableWorkstreamMutationRouter(operationId ? { operationId } : undefined);
5910
+ if (idempotencyKey)
5911
+ requireDurableExplicitIdempotency(router.enabled, 'workstream');
5519
5912
  let prepared;
5520
5913
  try {
5521
- prepared = planningCommandPreparationService.prepareWorkstreamCreate(args, {
5914
+ prepared = planningCommandPreparationService.prepareWorkstreamCreate(workstreamArgs, {
5522
5915
  durable: router.enabled,
5523
- entityId: requireStableMcpRequestId('workstream', 'create'),
5916
+ entityId: idempotencyKey
5917
+ ? stableExplicitMutationId('workstream', 'create_workstream', idempotencyKey)
5918
+ : requireStableMcpRequestId('workstream', 'create'),
5524
5919
  createdAt: new Date().toISOString(),
5525
5920
  });
5526
5921
  }
@@ -5576,43 +5971,64 @@ export function createTaskforceMcpServer(options = {}) {
5576
5971
  };
5577
5972
  }
5578
5973
  case "add_workstream_comment": {
5579
- const { id, text } = args;
5580
- const { id: workstreamId } = resolveWorkstreamByIdentifierOrThrow(id);
5581
- const validation = validateStructuredCommentForAdd(text);
5582
- if (!validation.ok) {
5583
- throw new McpPublicError(validation.message);
5584
- }
5585
- const effectiveAuthor = getActiveMcpActorRef();
5586
- const router = getDurableWorkstreamMutationRouter();
5587
- const updated = router.enabled
5588
- ? workstreamFromDurableResult(router.appendComment(workstreamId, {
5589
- author: effectiveAuthor,
5590
- submittedAuthor: effectiveAuthor,
5591
- text,
5592
- timestamp: new Date().toISOString(),
5593
- }))
5594
- : core.addWorkstreamComment(workstreamId, text, effectiveAuthor, ACTIVE_WORKSPACE_ID, { actorRef: effectiveAuthor });
5974
+ const { id, text, idempotencyKey: rawIdempotencyKey } = args;
5975
+ const { id: workstreamId } = measureToolSubphase('lookup', () => resolveWorkstreamByIdentifierOrThrow(id));
5976
+ const { effectiveAuthor, router, comment, explicitCommentId } = measureToolSubphase('prepare', () => {
5977
+ const validation = validateStructuredCommentForAdd(text);
5978
+ if (!validation.ok) {
5979
+ throw new McpPublicError(validation.message);
5980
+ }
5981
+ const effectiveAuthor = getActiveMcpActorRef();
5982
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
5983
+ const explicitCommentId = idempotencyKey
5984
+ ? stableExplicitMutationId('comment', 'add_workstream_comment', idempotencyKey)
5985
+ : null;
5986
+ const router = getDurableWorkstreamMutationRouter(idempotencyKey ? {
5987
+ operationId: stableExplicitMutationId('operation', 'add_workstream_comment', idempotencyKey),
5988
+ commentId: explicitCommentId,
5989
+ } : undefined);
5990
+ if (idempotencyKey)
5991
+ requireDurableExplicitIdempotency(router.enabled, 'workstream comment');
5992
+ return {
5993
+ effectiveAuthor,
5994
+ router,
5995
+ explicitCommentId,
5996
+ comment: {
5997
+ author: effectiveAuthor,
5998
+ submittedAuthor: effectiveAuthor,
5999
+ text,
6000
+ timestamp: new Date().toISOString(),
6001
+ },
6002
+ };
6003
+ });
6004
+ const updated = measureToolSubphase('durable-write', () => router.enabled
6005
+ ? workstreamFromDurableResult(router.appendComment(workstreamId, comment))
6006
+ : core.addWorkstreamComment(workstreamId, text, effectiveAuthor, ACTIVE_WORKSPACE_ID, { actorRef: effectiveAuthor }));
5595
6007
  if (!updated) {
5596
6008
  throw new McpPublicError(buildWorkstreamLookupFailureMessage(String(id || '').trim()), 'MCP_NOT_FOUND', 404);
5597
6009
  }
5598
- const persistedComment = Array.isArray(updated.comments) ? updated.comments.at(-1) : null;
5599
- recordMcpWriteAudit('add_workstream_comment', {
6010
+ const persistedComment = Array.isArray(updated.comments)
6011
+ ? (explicitCommentId
6012
+ ? updated.comments.find((candidate) => candidate.id === explicitCommentId) || null
6013
+ : updated.comments.at(-1) || null)
6014
+ : null;
6015
+ measureToolSubphase('audit', () => recordMcpWriteAudit('add_workstream_comment', {
5600
6016
  entityType: 'workstream',
5601
6017
  workstreamId: updated.id,
5602
6018
  commentId: persistedComment?.id || null,
5603
6019
  contentActorId: effectiveAuthor,
5604
- });
5605
- await notifyResourceMutation({
6020
+ }));
6021
+ await measureToolSubphaseAsync('notify', () => notifyResourceMutation({
5606
6022
  includeWorkstreams: true,
5607
6023
  workstreamIds: [updated.id],
5608
6024
  ...(updated.initiativeId ? { includeInitiatives: true, initiativeIds: [updated.initiativeId] } : {}),
5609
- });
5610
- return {
6025
+ }));
6026
+ return measureToolSubphase('serialize', () => ({
5611
6027
  content: [{ type: "text", text: JSON.stringify(buildPlanningWriteAcknowledgement('workstream', updated, 'add_workstream_comment', {
5612
6028
  changedFields: ['comments'],
5613
- comment: Array.isArray(updated.comments) ? updated.comments.at(-1) : null,
6029
+ comment: persistedComment,
5614
6030
  }), null, 2) }],
5615
- };
6031
+ }));
5616
6032
  }
5617
6033
  case "archive_workstream": {
5618
6034
  const { id } = args;
@@ -5655,34 +6071,54 @@ export function createTaskforceMcpServer(options = {}) {
5655
6071
  case "update_task_workstream": {
5656
6072
  const { id: legacyInputId, taskId, workstreamId } = args;
5657
6073
  const inputId = taskId ?? legacyInputId;
5658
- const { task } = resolveTaskByIdentifierOrThrow(inputId);
5659
- let prepared;
5660
- try {
5661
- prepared = planningCommandPreparationService.prepareTaskWorkstreamPlacement(task, workstreamId);
5662
- }
5663
- catch (error) {
5664
- rethrowPlanningPreparationError(error);
6074
+ const { task } = measureToolSubphase('lookup', () => resolveTaskByIdentifierOrThrow(inputId));
6075
+ const prepared = measureToolSubphase('prepare', () => {
6076
+ try {
6077
+ return planningCommandPreparationService.prepareTaskWorkstreamPlacement(task, workstreamId);
6078
+ }
6079
+ catch (error) {
6080
+ return rethrowPlanningPreparationError(error);
6081
+ }
6082
+ });
6083
+ if (prepared.nextWorkstreamId === prepared.previousWorkstreamId) {
6084
+ measureToolSubphase('audit', () => recordMcpWriteAudit('update_task_workstream', {
6085
+ taskId: task.id,
6086
+ workstreamId: prepared.nextWorkstreamId,
6087
+ previousWorkstreamId: prepared.previousWorkstreamId,
6088
+ noOp: true,
6089
+ }));
6090
+ return measureToolSubphase('serialize', () => ({
6091
+ content: [{
6092
+ type: "text",
6093
+ text: JSON.stringify({
6094
+ task: buildTaskWriteSummary(task),
6095
+ workstreamId: prepared.nextWorkstreamId,
6096
+ previousWorkstreamId: prepared.previousWorkstreamId,
6097
+ noOp: true,
6098
+ }, null, 2)
6099
+ }],
6100
+ }));
5665
6101
  }
5666
- const updated = mutateTaskThroughStatusBoundary({
6102
+ const updated = measureToolSubphase('durable-write', () => mutateTaskThroughStatusBoundary({
5667
6103
  task,
5668
6104
  updates: { workstreamId: prepared.nextWorkstreamId },
5669
- });
6105
+ }));
5670
6106
  if (!updated) {
5671
6107
  throw new McpPublicError(buildTaskLookupFailureMessage(String(inputId || '').trim()), 'MCP_NOT_FOUND', 404);
5672
6108
  }
5673
- recordMcpWriteAudit('update_task_workstream', {
6109
+ measureToolSubphase('audit', () => recordMcpWriteAudit('update_task_workstream', {
5674
6110
  taskId: updated.id,
5675
6111
  workstreamId: prepared.nextWorkstreamId,
5676
6112
  previousWorkstreamId: prepared.previousWorkstreamId,
5677
- });
5678
- await notifyResourceMutation({
6113
+ }));
6114
+ await measureToolSubphaseAsync('notify', () => notifyResourceMutation({
5679
6115
  includeTasks: true,
5680
6116
  taskIds: [updated.id],
5681
6117
  ...(prepared.affectedWorkstreamIds.length > 0
5682
6118
  ? { includeWorkstreams: true, workstreamIds: prepared.affectedWorkstreamIds }
5683
6119
  : {}),
5684
- });
5685
- return {
6120
+ }));
6121
+ return measureToolSubphase('serialize', () => ({
5686
6122
  content: [{
5687
6123
  type: "text",
5688
6124
  text: JSON.stringify({
@@ -5691,7 +6127,7 @@ export function createTaskforceMcpServer(options = {}) {
5691
6127
  previousWorkstreamId: prepared.previousWorkstreamId,
5692
6128
  }, null, 2)
5693
6129
  }],
5694
- };
6130
+ }));
5695
6131
  }
5696
6132
  case "update_workstream_initiative": {
5697
6133
  const { id: legacyInputId, workstreamId, initiativeId } = args;
@@ -5864,8 +6300,11 @@ export function createTaskforceMcpServer(options = {}) {
5864
6300
  }
5865
6301
  case "search_images": {
5866
6302
  assertAnnotatedAttachmentReadAccess();
5867
- const { query, limit } = args;
5868
- const results = searchImageAssets(query, { limit }).map(({ asset, matchedOn, score }) => ({
6303
+ const { query, limit, offset } = args;
6304
+ const page = normalizeSearchReadPage(limit, offset, 10);
6305
+ const queried = searchImageAssets(query, { limit: page.limit + 1, offset: page.offset });
6306
+ const hasMore = queried.length > page.limit;
6307
+ const results = (hasMore ? queried.slice(0, page.limit) : queried).map(({ asset, matchedOn, score }) => ({
5869
6308
  image: buildImageDescriptor(asset),
5870
6309
  matchedOn,
5871
6310
  score,
@@ -5881,6 +6320,13 @@ export function createTaskforceMcpServer(options = {}) {
5881
6320
  workspaceId: ACTIVE_WORKSPACE_ID,
5882
6321
  query: typeof query === 'string' ? query.trim() : '',
5883
6322
  results,
6323
+ pagination: {
6324
+ offset: page.offset,
6325
+ limit: page.limit,
6326
+ returned: results.length,
6327
+ hasMore,
6328
+ ...(hasMore ? { nextOffset: page.offset + results.length } : {}),
6329
+ },
5884
6330
  guidance: results.length > 0
5885
6331
  ? 'After picking a match, call get_image with the suggestedArgs.imageId to inspect linked tasks and image notes.'
5886
6332
  : 'No matching canonical images were found. Try an I-123 reference, title keyword, or filename fragment.',
@@ -5951,8 +6397,11 @@ export function createTaskforceMcpServer(options = {}) {
5951
6397
  };
5952
6398
  }
5953
6399
  case "search_documents": {
5954
- const { query, limit } = args;
5955
- const results = searchDocumentAssets(query, { limit }).map(({ asset, matchedOn, score }) => ({
6400
+ const { query, limit, offset } = args;
6401
+ const page = normalizeSearchReadPage(limit, offset, 10);
6402
+ const queried = searchDocumentAssets(query, { limit: page.limit + 1, offset: page.offset });
6403
+ const hasMore = queried.length > page.limit;
6404
+ const results = (hasMore ? queried.slice(0, page.limit) : queried).map(({ asset, matchedOn, score }) => ({
5956
6405
  document: buildDocumentDescriptor(asset),
5957
6406
  matchedOn,
5958
6407
  score,
@@ -5969,6 +6418,13 @@ export function createTaskforceMcpServer(options = {}) {
5969
6418
  workspaceId: ACTIVE_WORKSPACE_ID,
5970
6419
  query: typeof query === 'string' ? query.trim() : '',
5971
6420
  results,
6421
+ pagination: {
6422
+ offset: page.offset,
6423
+ limit: page.limit,
6424
+ returned: results.length,
6425
+ hasMore,
6426
+ ...(hasMore ? { nextOffset: page.offset + results.length } : {}),
6427
+ },
5972
6428
  guidance: results.length > 0
5973
6429
  ? 'After picking a match, call get_document with the suggestedArgs.documentId. Pass includeContent=true when you need the body.'
5974
6430
  : 'No matching canonical documents were found. Try a D-123 reference, document id, title keyword, or filename fragment.',
@@ -6585,7 +7041,7 @@ export function createTaskforceMcpServer(options = {}) {
6585
7041
  };
6586
7042
  }
6587
7043
  case "add_document_review_comment": {
6588
- const { sessionId, body, anchor, parentCommentId } = args;
7044
+ const { sessionId, body, anchor, parentCommentId, idempotencyKey: rawIdempotencyKey } = args;
6589
7045
  if (!sessionId || typeof sessionId !== 'string') {
6590
7046
  throw new McpPublicError('sessionId is required');
6591
7047
  }
@@ -6596,6 +7052,10 @@ export function createTaskforceMcpServer(options = {}) {
6596
7052
  && Object.prototype.hasOwnProperty.call(args, 'anchor') && anchor !== null && anchor !== undefined) {
6597
7053
  throw new McpPublicError('Replies cannot have anchors');
6598
7054
  }
7055
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
7056
+ const idempotentCommentId = idempotencyKey
7057
+ ? stableExplicitMutationId('comment', 'add_document_review_comment', idempotencyKey)
7058
+ : null;
6599
7059
  const session = typeof parentCommentId === 'string' && parentCommentId.trim()
6600
7060
  ? core.addDocumentReviewReply({
6601
7061
  workspaceId: ACTIVE_WORKSPACE_ID,
@@ -6603,6 +7063,8 @@ export function createTaskforceMcpServer(options = {}) {
6603
7063
  parentCommentId: parentCommentId.trim(),
6604
7064
  body,
6605
7065
  actorId: getActiveMcpActorRef(),
7066
+ idempotencyKey,
7067
+ commentId: idempotentCommentId,
6606
7068
  })
6607
7069
  : core.addDocumentReviewComment({
6608
7070
  workspaceId: ACTIVE_WORKSPACE_ID,
@@ -6610,9 +7072,13 @@ export function createTaskforceMcpServer(options = {}) {
6610
7072
  body,
6611
7073
  anchor: anchor ?? null,
6612
7074
  actorId: getActiveMcpActorRef(),
7075
+ idempotencyKey,
7076
+ commentId: idempotentCommentId,
6613
7077
  });
6614
7078
  const asset = workspaceAssetStore?.getDocument(session.assetId, ACTIVE_WORKSPACE_ID) || null;
6615
- const comment = session.comments.at(-1) || null;
7079
+ const comment = idempotentCommentId
7080
+ ? session.comments.find((candidate) => candidate.id === idempotentCommentId) || null
7081
+ : session.comments.at(-1) || null;
6616
7082
  recordMcpWriteAudit('add_document_review_comment', {
6617
7083
  sessionId: session.id,
6618
7084
  documentAssetId: session.assetId,
@@ -7150,17 +7616,24 @@ export function createTaskforceMcpServer(options = {}) {
7150
7616
  };
7151
7617
  }
7152
7618
  case "add_comment": {
7153
- const { id: inputId, text } = args;
7619
+ const { id: inputId, text, idempotencyKey: rawIdempotencyKey } = args;
7154
7620
  const { id, task } = resolveTaskByIdentifierOrThrow(inputId);
7155
7621
  const validation = validateStructuredCommentForAdd(text);
7156
7622
  if (!validation.ok) {
7157
7623
  throw new McpPublicError(validation.message);
7158
7624
  }
7159
7625
  const effectiveAuthor = getActiveMcpActorRef();
7160
- const operationId = requireStableMcpRequestId('operation', `task-comment:${id}`);
7161
- const commentId = requireStableMcpRequestId('comment', id);
7626
+ const idempotencyKey = normalizeExplicitIdempotencyKey(rawIdempotencyKey);
7627
+ const operationId = idempotencyKey
7628
+ ? stableExplicitMutationId('operation', 'add_comment', idempotencyKey)
7629
+ : requireStableMcpRequestId('operation', `task-comment:${id}`);
7630
+ const commentId = idempotencyKey
7631
+ ? stableExplicitMutationId('comment', 'add_comment', idempotencyKey)
7632
+ : requireStableMcpRequestId('comment', id);
7162
7633
  const existingCommentIds = new Set((task.comments || []).map((comment) => comment.id));
7163
7634
  const durableRouter = getDurableTaskCommentMutationRouter(effectiveAuthor, operationId);
7635
+ if (idempotencyKey)
7636
+ requireDurableExplicitIdempotency(durableRouter.enabled, 'task comment');
7164
7637
  const updated = durableRouter.enabled
7165
7638
  ? durableRouter.append({
7166
7639
  taskId: id,
@@ -7558,61 +8031,113 @@ export function createTaskforceMcpServer(options = {}) {
7558
8031
  const mcpInputValidator = createMcpInputValidator();
7559
8032
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
7560
8033
  const { name, arguments: args } = resolveMcpToolCallCompatibility(request.params.name, request.params.arguments);
8034
+ const toolStartedAtMs = Date.now();
8035
+ let preparationMs = 0;
8036
+ let validationMs = 0;
8037
+ let handlerMs = 0;
8038
+ let responseBytes = 0;
8039
+ let timingOutcome = 'success';
8040
+ const subphases = {};
7561
8041
  try {
7562
- assertToolProfile(name);
7563
- assertToolPermission(name);
7564
- const handler = toolHandlers.get(name);
7565
- if (!handler) {
7566
- throw new Error(`Unknown tool: ${name}`);
8042
+ const preparationStartedAtMs = Date.now();
8043
+ let handler;
8044
+ try {
8045
+ assertToolProfile(name);
8046
+ assertToolPermission(name);
8047
+ handler = toolHandlers.get(name);
8048
+ if (!handler) {
8049
+ throw new Error(`Unknown tool: ${name}`);
8050
+ }
8051
+ ensureResolvedProfileForTool(name, args);
8052
+ assertResolvedProfileForMutatingTool(name);
8053
+ }
8054
+ finally {
8055
+ preparationMs = Date.now() - preparationStartedAtMs;
7567
8056
  }
7568
- ensureResolvedProfileForTool(name, args);
7569
- assertResolvedProfileForMutatingTool(name);
7570
8057
  const execute = () => executeMcpToolWithReadRetry(name, (args || {}), async (validatedArgs) => {
8058
+ const validationStartedAtMs = Date.now();
7571
8059
  const descriptor = getCurrentToolDescriptor(name);
7572
8060
  if (!descriptor)
7573
8061
  throw new Error(`Unknown tool: ${name}`);
7574
8062
  const validation = mcpInputValidator.validate(name, descriptor.inputSchema, validatedArgs);
8063
+ validationMs += Date.now() - validationStartedAtMs;
7575
8064
  if (!validation.valid) {
7576
8065
  throw new McpPublicError(`Arguments for ${name} do not match the declared input schema.`, 'MCP_INVALID_ARGUMENT', 400, { issues: validation.issues });
7577
8066
  }
7578
- return await handler(stripMcpEnvelopeArgsForHandler(name, validatedArgs));
8067
+ const handlerStartedAtMs = Date.now();
8068
+ try {
8069
+ return await mcpToolSubphaseTimings.run(subphases, () => handler(stripMcpEnvelopeArgsForHandler(name, validatedArgs)));
8070
+ }
8071
+ finally {
8072
+ handlerMs += Date.now() - handlerStartedAtMs;
8073
+ }
7579
8074
  });
7580
- if (extra.requestId === undefined || extra.requestId === null)
7581
- return await execute();
7582
- const transportSessionId = String(extra.sessionId || '').trim();
7583
- const requestIdentity = operationIdentityScope
7584
- ? fingerprintWorkspaceSyncPayload({
7585
- kind: 'shared-mcp-request',
7586
- operationIdentityScope,
7587
- transportSessionId: transportSessionId
7588
- || options.auth?.mcpConnectionId
7589
- || options.auth?.mcpClientId
7590
- || 'mcp',
7591
- requestId: { type: typeof extra.requestId, value: extra.requestId },
7592
- tool: name,
7593
- })
7594
- : [
7595
- ...(runtimeMode === 'local' || !transportSessionId ? [runtimeRequestNamespace] : []),
8075
+ let result;
8076
+ if (extra.requestId === undefined || extra.requestId === null) {
8077
+ result = await execute();
8078
+ }
8079
+ else {
8080
+ const transportSessionId = String(extra.sessionId || '').trim();
8081
+ const requestIdentity = operationIdentityScope
8082
+ ? fingerprintWorkspaceSyncPayload({
8083
+ kind: 'shared-mcp-request',
8084
+ operationIdentityScope,
8085
+ transportSessionId: transportSessionId
8086
+ || options.auth?.mcpConnectionId
8087
+ || options.auth?.mcpClientId
8088
+ || 'mcp',
8089
+ requestId: { type: typeof extra.requestId, value: extra.requestId },
8090
+ tool: name,
8091
+ })
8092
+ : [
8093
+ ...(runtimeMode === 'local' || !transportSessionId ? [runtimeRequestNamespace] : []),
8094
+ String(transportSessionId || options.auth?.mcpConnectionId || options.auth?.mcpClientId || 'mcp'),
8095
+ String(extra.requestId),
8096
+ name,
8097
+ ].join(':');
8098
+ const logicalCloudCreateIdentity = [
7596
8099
  String(transportSessionId || options.auth?.mcpConnectionId || options.auth?.mcpClientId || 'mcp'),
7597
8100
  String(extra.requestId),
7598
8101
  name,
7599
8102
  ].join(':');
7600
- const logicalCloudCreateIdentity = [
7601
- String(transportSessionId || options.auth?.mcpConnectionId || options.auth?.mcpClientId || 'mcp'),
7602
- String(extra.requestId),
7603
- name,
7604
- ].join(':');
7605
- return await cloudCreateRequestIdentity.run(logicalCloudCreateIdentity, () => mcpRequestIdentity.run(requestIdentity, execute));
8103
+ result = await cloudCreateRequestIdentity.run(logicalCloudCreateIdentity, () => mcpRequestIdentity.run(requestIdentity, execute));
8104
+ }
8105
+ responseBytes = measureToolResultBytes(result);
8106
+ return result;
7606
8107
  }
7607
8108
  catch (error) {
8109
+ timingOutcome = 'failure';
7608
8110
  const recoverable = buildRecoverableToolErrorResult(error, auditRequestId);
7609
8111
  if (recoverable) {
7610
- return {
8112
+ const result = {
7611
8113
  ...recoverable,
7612
8114
  isError: true,
7613
8115
  };
8116
+ responseBytes = measureToolResultBytes(result);
8117
+ return result;
8118
+ }
8119
+ const result = buildUnexpectedToolErrorResult(auditRequestId || extra.requestId);
8120
+ responseBytes = measureToolResultBytes(result);
8121
+ return result;
8122
+ }
8123
+ finally {
8124
+ try {
8125
+ options.onToolCallTiming?.({
8126
+ requestId: String(auditRequestId || extra.requestId || '').trim() || null,
8127
+ toolName: name,
8128
+ isWrite: isMcpWriteToolName(name),
8129
+ outcome: timingOutcome,
8130
+ totalMs: Date.now() - toolStartedAtMs,
8131
+ preparationMs,
8132
+ validationMs,
8133
+ handlerMs,
8134
+ responseBytes,
8135
+ subphases: { ...subphases },
8136
+ });
8137
+ }
8138
+ catch {
8139
+ // Observability must never change tool behavior.
7614
8140
  }
7615
- return buildUnexpectedToolErrorResult(auditRequestId || extra.requestId);
7616
8141
  }
7617
8142
  });
7618
8143
  return {