@taskforcehq/taskforce 0.3.304 → 0.3.306

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 (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -1,4 +1,16 @@
1
1
  import { isAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
2
+ import { isAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
3
+ import { normalizeSyncSameOriginAssetUrl } from '../sync/syncFieldSemantics.js';
4
+ function normalizeNullableIsoString(value) {
5
+ if (typeof value !== 'string')
6
+ return null;
7
+ const normalized = value.trim();
8
+ return normalized.length > 0 ? normalized : null;
9
+ }
10
+ function normalizeNullableProfileReference(value) {
11
+ const normalized = String(value || '').trim();
12
+ return normalized.length > 0 ? normalized : null;
13
+ }
2
14
  function normalizeOptionalProfileField(value) {
3
15
  if (value === undefined)
4
16
  return undefined;
@@ -7,6 +19,34 @@ function normalizeOptionalProfileField(value) {
7
19
  const normalized = String(value).trim();
8
20
  return normalized.length > 0 ? normalized : undefined;
9
21
  }
22
+ function normalizeAiProfileAvatarUrl(value) {
23
+ return normalizeSyncSameOriginAssetUrl(value);
24
+ }
25
+ function compareIsoStrings(left, right) {
26
+ const normalizedLeft = normalizeNullableIsoString(left);
27
+ const normalizedRight = normalizeNullableIsoString(right);
28
+ if (!normalizedLeft && !normalizedRight)
29
+ return 0;
30
+ if (!normalizedLeft)
31
+ return -1;
32
+ if (!normalizedRight)
33
+ return 1;
34
+ return normalizedLeft.localeCompare(normalizedRight);
35
+ }
36
+ function deriveRosterStateUpdatedAt(value, fallbacks) {
37
+ const explicitValue = normalizeNullableIsoString(value);
38
+ const archivedAt = normalizeNullableIsoString(fallbacks.archivedAt);
39
+ const createdAt = normalizeNullableIsoString(fallbacks.createdAt);
40
+ if (archivedAt) {
41
+ return explicitValue && compareIsoStrings(explicitValue, archivedAt) >= 0
42
+ ? explicitValue
43
+ : archivedAt;
44
+ }
45
+ // Legacy active snapshots may carry fresh metadata updatedAt values without
46
+ // carrying an intentional roster lifecycle event. Treat creation as the
47
+ // active lifecycle baseline so metadata-only sync cannot resurrect archives.
48
+ return explicitValue ?? createdAt;
49
+ }
10
50
  function normalizeAiProfileSurfaceTypeInput(deps, value) {
11
51
  if (value === undefined)
12
52
  return undefined;
@@ -23,8 +63,218 @@ function normalizeAiProfileSurfaceTypeInput(deps, value) {
23
63
  }
24
64
  return normalized;
25
65
  }
26
- export function listAiProfilesService(deps, workspaceId) {
66
+ function normalizeAiProfileSeatScopeInput(deps, value) {
67
+ if (value === undefined)
68
+ return undefined;
69
+ if (value === null)
70
+ return null;
71
+ if (typeof value !== 'string') {
72
+ throw deps.createRuleError('AI profile seatScope must be cloud_metered or local_unmetered', 400, 'AI_PROFILE_SEAT_SCOPE_INVALID');
73
+ }
74
+ const normalized = value.trim();
75
+ if (!normalized)
76
+ return null;
77
+ if (!isAiProfileSeatScope(normalized)) {
78
+ throw deps.createRuleError('AI profile seatScope must be cloud_metered or local_unmetered', 400, 'AI_PROFILE_SEAT_SCOPE_INVALID');
79
+ }
80
+ return normalized;
81
+ }
82
+ function resolveAiProfileSeatScope(deps, inputSeatScope, existing) {
83
+ const incomingSeatScope = normalizeAiProfileSeatScopeInput(deps, inputSeatScope);
84
+ const existingSeatScope = existing?.seatScope ?? null;
85
+ if (incomingSeatScope === undefined) {
86
+ return {
87
+ seatScope: existingSeatScope,
88
+ seatScopeProvided: false,
89
+ };
90
+ }
91
+ if (incomingSeatScope === 'cloud_metered' || existingSeatScope === 'cloud_metered') {
92
+ return {
93
+ seatScope: 'cloud_metered',
94
+ seatScopeProvided: true,
95
+ };
96
+ }
97
+ return {
98
+ seatScope: incomingSeatScope ?? existingSeatScope,
99
+ seatScopeProvided: true,
100
+ };
101
+ }
102
+ function resolveSyncedAiProfileSeatScope(deps, inputSeatScope, existing) {
103
+ const incomingSeatScope = normalizeAiProfileSeatScopeInput(deps, inputSeatScope);
104
+ const existingSeatScope = existing?.seatScope ?? null;
105
+ if (incomingSeatScope === undefined) {
106
+ return {
107
+ seatScope: existingSeatScope,
108
+ seatScopeProvided: false,
109
+ };
110
+ }
111
+ return {
112
+ seatScope: incomingSeatScope ?? existingSeatScope,
113
+ seatScopeProvided: true,
114
+ };
115
+ }
116
+ function resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existing) {
117
+ const normalizedTrustedSeatScope = normalizeAiProfileSeatScopeInput(deps, trustedSeatScope);
118
+ if (normalizedTrustedSeatScope === undefined) {
119
+ return {
120
+ seatScope: existing?.seatScope ?? null,
121
+ seatScopeProvided: false,
122
+ };
123
+ }
124
+ return {
125
+ seatScope: normalizedTrustedSeatScope,
126
+ seatScopeProvided: true,
127
+ };
128
+ }
129
+ function aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope) {
130
+ if (trustedSeatScope === undefined)
131
+ return true;
132
+ const profile = deps.aiProfileRowToRecord(row);
133
+ const profileSeatScope = profile.seatScope ?? null;
134
+ if (profileSeatScope === null)
135
+ return true;
136
+ return profileSeatScope === trustedSeatScope;
137
+ }
138
+ function findFirstAiProfileRowForTrustedSeatScope(deps, rows, trustedSeatScope) {
139
+ if (trustedSeatScope === undefined)
140
+ return rows[0];
141
+ return rows.find((row) => aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope));
142
+ }
143
+ function filterAiProfileRowsForTrustedSeatScope(deps, rows, trustedSeatScope) {
144
+ if (trustedSeatScope === undefined)
145
+ return rows;
146
+ return rows.filter((row) => aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope));
147
+ }
148
+ function requiresCloudSeatReservation(existing, nextSeatScope) {
149
+ if (nextSeatScope !== 'cloud_metered')
150
+ return false;
151
+ if (existing?.archivedAt)
152
+ return true;
153
+ return existing?.seatScope !== 'cloud_metered';
154
+ }
155
+ function assertAiProfileSeatAvailable(deps, workspaceId) {
156
+ const aiProfileUsage = deps.countAiProfileUsage(workspaceId);
157
+ if (aiProfileUsage.limit !== null && aiProfileUsage.used >= aiProfileUsage.limit) {
158
+ throw deps.createRuleError('No AI seats are available for this workspace. Remove an existing cloud AI profile or upgrade your plan.', 403, 'AI_PROFILE_LIMIT_REACHED', {
159
+ workspaceId,
160
+ limit: aiProfileUsage.limit,
161
+ used: aiProfileUsage.used,
162
+ remaining: aiProfileUsage.remaining
163
+ });
164
+ }
165
+ }
166
+ function inferAiProfileProviderModelDefaults(name) {
167
+ const normalized = String(name || '').trim().toLowerCase();
168
+ if (!normalized)
169
+ return {};
170
+ if (normalized.includes('claude code'))
171
+ return { provider: 'Anthropic', model: 'Claude Code' };
172
+ if (normalized.includes('claude'))
173
+ return { provider: 'Anthropic', model: 'Claude' };
174
+ if (normalized.includes('chatgpt'))
175
+ return { provider: 'OpenAI', model: 'ChatGPT' };
176
+ if (normalized.includes('codex'))
177
+ return { provider: 'OpenAI', model: 'Codex' };
178
+ if (normalized.includes('gemini'))
179
+ return { provider: 'Google', model: 'Gemini' };
180
+ return {};
181
+ }
182
+ function inferAiProfileMetadataDefaults(name, surfaceType) {
183
+ const inferredProviderModel = inferAiProfileProviderModelDefaults(name);
184
+ switch (surfaceType) {
185
+ case 'agent':
186
+ return {
187
+ description: `${name} AI collaborator registered in Taskforce.`,
188
+ role: 'AI Collaborator',
189
+ ...inferredProviderModel
190
+ };
191
+ case 'coding_tool':
192
+ return {
193
+ description: `${name} coding agent connected through MCP.`,
194
+ role: 'Coding Agent',
195
+ ...inferredProviderModel
196
+ };
197
+ case 'chat_app':
198
+ return {
199
+ description: `${name} conversational AI connected through Taskforce.`,
200
+ role: 'AI Assistant',
201
+ ...inferredProviderModel
202
+ };
203
+ case 'ide':
204
+ return {
205
+ description: `${name} IDE-connected AI assistant.`,
206
+ role: 'Coding Assistant',
207
+ ...inferredProviderModel
208
+ };
209
+ case 'other':
210
+ return {
211
+ description: `${name} AI profile connected through Taskforce.`,
212
+ role: 'AI Assistant',
213
+ ...inferredProviderModel
214
+ };
215
+ default:
216
+ return {};
217
+ }
218
+ }
219
+ function resolveOptionalProfileField(inputValue, existingValue, fallbackValue) {
220
+ const explicitValue = normalizeOptionalProfileField(inputValue);
221
+ if (explicitValue !== undefined)
222
+ return explicitValue;
223
+ const preservedValue = normalizeOptionalProfileField(existingValue);
224
+ if (preservedValue !== undefined)
225
+ return preservedValue;
226
+ return normalizeOptionalProfileField(fallbackValue);
227
+ }
228
+ function resolveAiProfileMetadataFields(deps, input, existing) {
229
+ const surfaceTypeInput = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
230
+ const effectiveSurfaceType = surfaceTypeInput === undefined
231
+ ? existing?.surfaceType ?? null
232
+ : surfaceTypeInput;
233
+ const inferredDefaults = inferAiProfileMetadataDefaults(input.name, effectiveSurfaceType);
234
+ return {
235
+ description: resolveOptionalProfileField(input.description, existing?.description, inferredDefaults.description) ?? null,
236
+ role: resolveOptionalProfileField(input.role, existing?.role, inferredDefaults.role) ?? null,
237
+ provider: resolveOptionalProfileField(input.provider, existing?.provider, inferredDefaults.provider) ?? null,
238
+ model: resolveOptionalProfileField(input.model, existing?.model, inferredDefaults.model) ?? null,
239
+ surfaceType: effectiveSurfaceType,
240
+ surfaceTypeProvided: surfaceTypeInput !== undefined,
241
+ };
242
+ }
243
+ function applyAiProfileMetadataDefaults(record) {
244
+ const inferredDefaults = inferAiProfileMetadataDefaults(record.name, record.surfaceType);
245
+ return {
246
+ ...record,
247
+ description: resolveOptionalProfileField(undefined, record.description, inferredDefaults.description) ?? null,
248
+ role: resolveOptionalProfileField(undefined, record.role, inferredDefaults.role) ?? null,
249
+ provider: resolveOptionalProfileField(undefined, record.provider, inferredDefaults.provider) ?? null,
250
+ model: resolveOptionalProfileField(undefined, record.model, inferredDefaults.model) ?? null,
251
+ };
252
+ }
253
+ function hydrateAiProfileRecord(deps, row, diagnostics) {
254
+ const record = deps.aiProfileRowToRecord(row);
255
+ const hydrated = applyAiProfileMetadataDefaults(record);
256
+ const metadataChanged = record.description !== hydrated.description
257
+ || record.role !== hydrated.role
258
+ || record.provider !== hydrated.provider
259
+ || record.model !== hydrated.model;
260
+ if (metadataChanged) {
261
+ deps.db.prepare(`
262
+ UPDATE ai_profiles
263
+ SET description = ?,
264
+ role = ?,
265
+ provider = ?,
266
+ model = ?
267
+ WHERE tenant_id = ? AND id = ?
268
+ `).run(hydrated.description ?? null, hydrated.role ?? null, hydrated.provider ?? null, hydrated.model ?? null, deps.tenantId, record.id);
269
+ if (diagnostics) {
270
+ diagnostics.metadataBackfillCount = (diagnostics.metadataBackfillCount || 0) + 1;
271
+ }
272
+ }
273
+ return hydrated;
274
+ }
275
+ export function listAiProfilesService(deps, workspaceId, options) {
27
276
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
277
+ const includeArchived = options?.includeArchived === true;
28
278
  const rows = deps.db.prepare(`
29
279
  SELECT
30
280
  id,
@@ -34,36 +284,49 @@ export function listAiProfilesService(deps, workspaceId) {
34
284
  username,
35
285
  icon,
36
286
  color,
287
+ avatar_url,
288
+ avatar_source_url,
37
289
  description,
38
290
  role,
39
291
  provider,
40
292
  model,
41
293
  surface_type,
294
+ seat_scope,
42
295
  provider_metadata_json,
296
+ archived_at,
297
+ archived_by,
298
+ archived_reason,
299
+ merged_into_profile_id,
300
+ roster_state_updated_at,
43
301
  created_at,
44
302
  updated_at,
45
303
  last_active_at
46
304
  FROM ai_profiles
47
305
  WHERE tenant_id = ?
48
306
  AND workspace_id = ?
307
+ AND (? = 1 OR archived_at IS NULL)
49
308
  ORDER BY COALESCE(updated_at, created_at) DESC, LOWER(name) ASC
50
- `).all(deps.tenantId, normalizedWorkspaceId);
51
- return rows.map((row) => deps.aiProfileRowToRecord(row));
309
+ `).all(deps.tenantId, normalizedWorkspaceId, includeArchived ? 1 : 0);
310
+ return rows.map((row) => hydrateAiProfileRecord(deps, row, options?.diagnostics));
52
311
  }
53
312
  export function getAiProfileByTokenService(deps, input) {
54
313
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
55
314
  const profileToken = String(input.profileToken || '').trim();
315
+ const includeArchived = input.includeArchived === true;
316
+ const trustedSeatScope = normalizeAiProfileSeatScopeInput(deps, input.seatScope);
56
317
  if (!profileToken)
57
318
  return null;
58
- const row = deps.db.prepare(`
319
+ const rows = deps.db.prepare(`
59
320
  SELECT *
60
321
  FROM ai_profiles
61
322
  WHERE tenant_id = ?
62
323
  AND workspace_id = ?
63
324
  AND profile_token = ?
64
- LIMIT 1
65
- `).get(deps.tenantId, normalizedWorkspaceId, profileToken);
66
- return row ? deps.aiProfileRowToRecord(row) : null;
325
+ AND (? = 1 OR archived_at IS NULL)
326
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
327
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken, includeArchived ? 1 : 0);
328
+ const row = findFirstAiProfileRowForTrustedSeatScope(deps, rows, trustedSeatScope);
329
+ return row ? hydrateAiProfileRecord(deps, row) : null;
67
330
  }
68
331
  export function upsertAiProfileForSyncService(deps, profile) {
69
332
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(profile.workspaceId);
@@ -77,24 +340,144 @@ export function upsertAiProfileForSyncService(deps, profile) {
77
340
  const normalizedLastActiveAt = typeof profile.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
78
341
  ? profile.lastActiveAt.trim()
79
342
  : null;
80
- const normalizedSurfaceType = normalizeAiProfileSurfaceTypeInput(deps, profile.surfaceType);
343
+ const normalizedArchivedAt = normalizeNullableIsoString(profile.archivedAt);
344
+ const normalizedArchivedBy = normalizeNullableProfileReference(profile.archivedBy);
345
+ const normalizedArchivedReason = normalizeOptionalProfileField(profile.archivedReason) ?? null;
346
+ const normalizedMergedIntoProfileId = normalizeNullableProfileReference(profile.mergedIntoProfileId);
347
+ const normalizedRosterStateUpdatedAt = deriveRosterStateUpdatedAt(profile.rosterStateUpdatedAt, {
348
+ archivedAt: normalizedArchivedAt,
349
+ createdAt: normalizedCreatedAt,
350
+ updatedAt: normalizedUpdatedAt,
351
+ });
81
352
  const existingRow = deps.db.prepare(`
82
353
  SELECT *
83
354
  FROM ai_profiles
84
355
  WHERE tenant_id = ? AND workspace_id = ? AND id = ?
85
356
  LIMIT 1
86
357
  `).get(deps.tenantId, normalizedWorkspaceId, profileId);
87
- if (existingRow) {
88
- const existingUpdatedAt = String(existingRow.updated_at || existingRow.created_at || '').trim();
89
- if (existingUpdatedAt && existingUpdatedAt > normalizedUpdatedAt) {
90
- return deps.aiProfileRowToRecord(existingRow);
358
+ const existingProfile = existingRow ? deps.aiProfileRowToRecord(existingRow) : null;
359
+ const incomingAvatarUrlProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarUrl');
360
+ const incomingAvatarSourceUrlProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarSourceUrl');
361
+ const existingCreatedAt = existingRow
362
+ ? String(existingRow.created_at || existingRow.updated_at || '').trim()
363
+ : '';
364
+ const existingUpdatedAt = existingRow
365
+ ? String(existingRow.updated_at || existingRow.created_at || '').trim()
366
+ : '';
367
+ const existingLastActiveAt = existingRow
368
+ ? normalizeNullableIsoString(existingRow.last_active_at)
369
+ : null;
370
+ const existingArchivedAt = existingRow
371
+ ? normalizeNullableIsoString(existingRow.archived_at)
372
+ : null;
373
+ const existingArchivedBy = existingRow
374
+ ? normalizeNullableProfileReference(existingRow.archived_by)
375
+ : null;
376
+ const existingArchivedReason = existingRow
377
+ ? normalizeOptionalProfileField(existingRow.archived_reason) ?? null
378
+ : null;
379
+ const existingMergedIntoProfileId = existingRow
380
+ ? normalizeNullableProfileReference(existingRow.merged_into_profile_id)
381
+ : null;
382
+ const existingRosterStateUpdatedAt = existingRow
383
+ ? deriveRosterStateUpdatedAt(existingRow.roster_state_updated_at, {
384
+ archivedAt: existingArchivedAt,
385
+ createdAt: existingCreatedAt,
386
+ updatedAt: existingUpdatedAt,
387
+ })
388
+ : null;
389
+ const metadataComparison = compareIsoStrings(normalizedUpdatedAt, existingUpdatedAt);
390
+ const incomingMetadataWins = !existingRow || metadataComparison > 0;
391
+ const lifecycleComparison = compareIsoStrings(normalizedRosterStateUpdatedAt, existingRosterStateUpdatedAt);
392
+ const incomingArchived = normalizedArchivedAt !== null;
393
+ const existingArchived = existingArchivedAt !== null;
394
+ const incomingLifecycleWins = !existingRow
395
+ || lifecycleComparison > 0
396
+ || (lifecycleComparison === 0 && incomingArchived && !existingArchived);
397
+ const metadataSourceProfile = incomingMetadataWins
398
+ ? {
399
+ id: profileId,
400
+ workspaceId: normalizedWorkspaceId,
401
+ profileToken: String(profile.profileToken || '').trim(),
402
+ name: deps.normalizeAiProfileName(profile.name),
403
+ username: deps.normalizeAiProfileUsername(profile.username, profile.name),
404
+ icon: deps.normalizeAiProfileIcon(profile.icon),
405
+ color: deps.normalizeAiProfileColor(profile.color),
406
+ avatarUrl: incomingAvatarUrlProvided
407
+ ? normalizeAiProfileAvatarUrl(profile.avatarUrl)
408
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarUrl),
409
+ avatarSourceUrl: incomingAvatarSourceUrlProvided
410
+ ? normalizeAiProfileAvatarUrl(profile.avatarSourceUrl)
411
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarSourceUrl),
412
+ description: profile.description ?? null,
413
+ role: profile.role ?? null,
414
+ provider: profile.provider ?? null,
415
+ model: profile.model ?? null,
416
+ surfaceType: profile.surfaceType ?? null,
417
+ seatScope: profile.seatScope ?? null,
418
+ providerMetadata: profile.providerMetadata ?? null,
419
+ archivedAt: normalizedArchivedAt,
420
+ archivedBy: normalizedArchivedBy,
421
+ archivedReason: normalizedArchivedReason,
422
+ mergedIntoProfileId: normalizedMergedIntoProfileId,
423
+ rosterStateUpdatedAt: normalizedRosterStateUpdatedAt,
424
+ createdAt: normalizedCreatedAt,
425
+ updatedAt: normalizedUpdatedAt,
426
+ lastActiveAt: normalizedLastActiveAt
91
427
  }
92
- }
428
+ : existingProfile;
429
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
430
+ name: metadataSourceProfile?.name ?? deps.normalizeAiProfileName(profile.name),
431
+ description: metadataSourceProfile?.description,
432
+ role: metadataSourceProfile?.role,
433
+ provider: metadataSourceProfile?.provider,
434
+ model: metadataSourceProfile?.model,
435
+ surfaceType: metadataSourceProfile?.surfaceType,
436
+ }, existingProfile);
437
+ const resolvedSeatScope = incomingMetadataWins
438
+ ? resolveSyncedAiProfileSeatScope(deps, profile.seatScope, existingProfile)
439
+ : {
440
+ seatScope: existingProfile?.seatScope ?? null,
441
+ seatScopeProvided: existingProfile?.seatScope !== undefined,
442
+ };
443
+ const mergedProfileToken = incomingMetadataWins
444
+ ? String(profile.profileToken || '').trim()
445
+ : String(existingRow?.profile_token || '').trim();
446
+ const mergedName = metadataSourceProfile?.name ?? deps.normalizeAiProfileName(profile.name);
447
+ const mergedUsername = metadataSourceProfile?.username ?? deps.normalizeAiProfileUsername(profile.username, profile.name);
448
+ const mergedIcon = metadataSourceProfile?.icon ?? deps.normalizeAiProfileIcon(profile.icon);
449
+ const mergedColor = metadataSourceProfile?.color ?? deps.normalizeAiProfileColor(profile.color);
450
+ const incomingAvatarFieldsWin = incomingMetadataWins || metadataComparison === 0;
451
+ const mergedAvatarUrl = incomingAvatarUrlProvided && incomingAvatarFieldsWin
452
+ ? normalizeAiProfileAvatarUrl(profile.avatarUrl)
453
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarUrl);
454
+ const mergedAvatarSourceUrl = incomingAvatarSourceUrlProvided && incomingAvatarFieldsWin
455
+ ? normalizeAiProfileAvatarUrl(profile.avatarSourceUrl)
456
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarSourceUrl);
457
+ const mergedProviderMetadata = incomingMetadataWins
458
+ ? (profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
459
+ ? JSON.stringify(profile.providerMetadata)
460
+ : null)
461
+ : (typeof existingRow?.provider_metadata_json === 'string' ? existingRow.provider_metadata_json : null);
462
+ const mergedArchivedAt = incomingLifecycleWins ? normalizedArchivedAt : existingArchivedAt;
463
+ const mergedArchivedBy = incomingLifecycleWins ? normalizedArchivedBy : existingArchivedBy;
464
+ const mergedArchivedReason = incomingLifecycleWins ? normalizedArchivedReason : existingArchivedReason;
465
+ const mergedMergedIntoProfileId = incomingLifecycleWins ? normalizedMergedIntoProfileId : existingMergedIntoProfileId;
466
+ const mergedRosterStateUpdatedAt = incomingLifecycleWins
467
+ ? normalizedRosterStateUpdatedAt
468
+ : existingRosterStateUpdatedAt;
469
+ const mergedCreatedAt = existingCreatedAt || normalizedCreatedAt;
470
+ const mergedUpdatedAt = incomingMetadataWins ? normalizedUpdatedAt : (existingUpdatedAt || normalizedUpdatedAt);
471
+ const mergedLastActiveAt = compareIsoStrings(normalizedLastActiveAt, existingLastActiveAt) > 0
472
+ ? normalizedLastActiveAt
473
+ : existingLastActiveAt;
93
474
  deps.db.prepare(`
94
475
  INSERT INTO ai_profiles (
95
476
  id, tenant_id, workspace_id, profile_token, name, username, icon, color,
96
- description, role, provider, model, surface_type, provider_metadata_json, created_at, updated_at, last_active_at
97
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
477
+ avatar_url, avatar_source_url, description, role, provider, model, surface_type, seat_scope, provider_metadata_json,
478
+ archived_at, archived_by, archived_reason, merged_into_profile_id,
479
+ roster_state_updated_at, created_at, updated_at, last_active_at
480
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
98
481
  ON CONFLICT(id) DO UPDATE SET
99
482
  workspace_id = excluded.workspace_id,
100
483
  profile_token = excluded.profile_token,
@@ -102,18 +485,24 @@ export function upsertAiProfileForSyncService(deps, profile) {
102
485
  username = excluded.username,
103
486
  icon = excluded.icon,
104
487
  color = excluded.color,
488
+ avatar_url = excluded.avatar_url,
489
+ avatar_source_url = excluded.avatar_source_url,
105
490
  description = excluded.description,
106
491
  role = excluded.role,
107
492
  provider = excluded.provider,
108
493
  model = excluded.model,
109
494
  surface_type = excluded.surface_type,
495
+ seat_scope = excluded.seat_scope,
110
496
  provider_metadata_json = excluded.provider_metadata_json,
497
+ archived_at = excluded.archived_at,
498
+ archived_by = excluded.archived_by,
499
+ archived_reason = excluded.archived_reason,
500
+ merged_into_profile_id = excluded.merged_into_profile_id,
501
+ roster_state_updated_at = excluded.roster_state_updated_at,
111
502
  created_at = excluded.created_at,
112
503
  updated_at = excluded.updated_at,
113
504
  last_active_at = excluded.last_active_at
114
- `).run(profileId, deps.tenantId, normalizedWorkspaceId, String(profile.profileToken || '').trim(), deps.normalizeAiProfileName(profile.name), deps.normalizeAiProfileUsername(profile.username, profile.name), deps.normalizeAiProfileIcon(profile.icon), deps.normalizeAiProfileColor(profile.color), normalizeOptionalProfileField(profile.description) ?? null, normalizeOptionalProfileField(profile.role) ?? null, normalizeOptionalProfileField(profile.provider) ?? null, normalizeOptionalProfileField(profile.model) ?? null, normalizedSurfaceType, profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
115
- ? JSON.stringify(profile.providerMetadata)
116
- : null, normalizedCreatedAt, normalizedUpdatedAt, normalizedLastActiveAt);
505
+ `).run(profileId, deps.tenantId, normalizedWorkspaceId, mergedProfileToken, mergedName, mergedUsername, mergedIcon, mergedColor, mergedAvatarUrl, mergedAvatarSourceUrl, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceType, resolvedSeatScope.seatScope, mergedProviderMetadata, mergedArchivedAt, mergedArchivedBy, mergedArchivedReason, mergedMergedIntoProfileId, mergedRosterStateUpdatedAt, mergedCreatedAt, mergedUpdatedAt, mergedLastActiveAt);
117
506
  const refreshed = deps.db.prepare(`
118
507
  SELECT *
119
508
  FROM ai_profiles
@@ -123,7 +512,7 @@ export function upsertAiProfileForSyncService(deps, profile) {
123
512
  if (!refreshed) {
124
513
  throw deps.createRuleError('Failed to upsert AI profile during sync', 500, 'AI_PROFILE_SYNC_UPSERT_FAILED');
125
514
  }
126
- return deps.aiProfileRowToRecord(refreshed);
515
+ return hydrateAiProfileRecord(deps, refreshed);
127
516
  }
128
517
  export function mergeAiProfilesService(deps, input) {
129
518
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
@@ -134,15 +523,18 @@ export function mergeAiProfilesService(deps, input) {
134
523
  if (keepId === mergeId)
135
524
  throw deps.createRuleError('keepId and mergeId must be different profiles', 400, 'MERGE_PROFILES_SAME_ID');
136
525
  const keepProfile = deps.db.prepare(`
137
- SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? LIMIT 1
526
+ SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL LIMIT 1
138
527
  `).get(deps.tenantId, normalizedWorkspaceId, keepId);
139
528
  if (!keepProfile)
140
529
  throw deps.createRuleError(`Profile ${keepId} not found`, 404, 'MERGE_PROFILES_KEEP_NOT_FOUND');
141
530
  const mergeProfile = deps.db.prepare(`
142
- SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? LIMIT 1
531
+ SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL LIMIT 1
143
532
  `).get(deps.tenantId, normalizedWorkspaceId, mergeId);
144
533
  if (!mergeProfile)
145
534
  throw deps.createRuleError(`Profile ${mergeId} not found`, 404, 'MERGE_PROFILES_MERGE_NOT_FOUND');
535
+ const keepRecord = deps.aiProfileRowToRecord(keepProfile);
536
+ const mergeRecord = deps.aiProfileRowToRecord(mergeProfile);
537
+ const mergedSeatScope = resolveAiProfileSeatScope(deps, mergeRecord.seatScope, keepRecord);
146
538
  const mergedAt = new Date().toISOString();
147
539
  const doMerge = deps.db.transaction(() => {
148
540
  deps.db.prepare(`
@@ -153,51 +545,330 @@ export function mergeAiProfilesService(deps, input) {
153
545
  UPDATE tasks SET created_by = ?, updated_at = ?
154
546
  WHERE tenant_id = ? AND workspace_id = ? AND created_by = ?
155
547
  `).run(keepId, mergedAt, deps.tenantId, normalizedWorkspaceId, mergeId);
548
+ deps.db.prepare(`
549
+ UPDATE ai_profiles
550
+ SET seat_scope = ?,
551
+ avatar_url = COALESCE(avatar_url, ?),
552
+ avatar_source_url = COALESCE(avatar_source_url, ?),
553
+ updated_at = ?
554
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
555
+ `).run(mergedSeatScope.seatScope, normalizeAiProfileAvatarUrl(mergeRecord.avatarUrl), normalizeAiProfileAvatarUrl(mergeRecord.avatarSourceUrl), mergedAt, deps.tenantId, normalizedWorkspaceId, keepId);
156
556
  deps.db.prepare(`
157
557
  UPDATE comments SET author = ?
158
558
  WHERE tenant_id = ? AND workspace_id = ? AND author = ?
159
559
  `).run(keepId, deps.tenantId, normalizedWorkspaceId, mergeId);
160
560
  deps.db.prepare(`
161
- DELETE FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ?
162
- `).run(deps.tenantId, normalizedWorkspaceId, mergeId);
561
+ UPDATE ai_profiles
562
+ SET archived_at = ?,
563
+ archived_by = ?,
564
+ archived_reason = ?,
565
+ merged_into_profile_id = ?,
566
+ roster_state_updated_at = ?,
567
+ updated_at = ?
568
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
569
+ `).run(mergedAt, keepId, 'merged_duplicate', keepId, mergedAt, mergedAt, deps.tenantId, normalizedWorkspaceId, mergeId);
163
570
  });
164
571
  doMerge();
165
572
  const refreshed = deps.db.prepare(`
166
573
  SELECT * FROM ai_profiles WHERE tenant_id = ? AND id = ? LIMIT 1
167
574
  `).get(deps.tenantId, keepId);
168
- return deps.aiProfileRowToRecord(refreshed);
575
+ return hydrateAiProfileRecord(deps, refreshed);
576
+ }
577
+ export function updateAiProfileAvatarService(deps, input) {
578
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
579
+ const profileId = String(input.profileId || '').trim();
580
+ if (!profileId)
581
+ throw deps.createRuleError('profileId is required', 400, 'AI_PROFILE_ID_REQUIRED');
582
+ const existing = deps.db.prepare(`
583
+ SELECT *
584
+ FROM ai_profiles
585
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL
586
+ LIMIT 1
587
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
588
+ if (!existing) {
589
+ throw deps.createRuleError(`Profile ${profileId} not found`, 404, 'AI_PROFILE_NOT_FOUND');
590
+ }
591
+ const avatarUrl = normalizeAiProfileAvatarUrl(input.avatarUrl);
592
+ if (typeof input.avatarUrl === 'string' && input.avatarUrl.trim().length > 0 && !avatarUrl) {
593
+ throw deps.createRuleError('avatarUrl must be a Taskforce context URL', 400, 'AI_PROFILE_AVATAR_URL_INVALID');
594
+ }
595
+ const avatarSourceUrl = input.avatarSourceUrl === undefined
596
+ ? normalizeAiProfileAvatarUrl(existing.avatar_source_url)
597
+ : normalizeAiProfileAvatarUrl(input.avatarSourceUrl);
598
+ if (typeof input.avatarSourceUrl === 'string' && input.avatarSourceUrl.trim().length > 0 && !avatarSourceUrl) {
599
+ throw deps.createRuleError('avatarSourceUrl must be a Taskforce context URL', 400, 'AI_PROFILE_AVATAR_SOURCE_URL_INVALID');
600
+ }
601
+ const updatedAt = new Date().toISOString();
602
+ deps.db.prepare(`
603
+ UPDATE ai_profiles
604
+ SET avatar_url = ?,
605
+ avatar_source_url = ?,
606
+ updated_at = ?
607
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
608
+ `).run(avatarUrl, avatarSourceUrl, updatedAt, deps.tenantId, normalizedWorkspaceId, profileId);
609
+ const refreshed = deps.db.prepare(`
610
+ SELECT *
611
+ FROM ai_profiles
612
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
613
+ LIMIT 1
614
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
615
+ if (!refreshed) {
616
+ throw deps.createRuleError('Failed to update AI profile avatar', 500, 'AI_PROFILE_AVATAR_UPDATE_FAILED');
617
+ }
618
+ return hydrateAiProfileRecord(deps, refreshed);
619
+ }
620
+ export function archiveAiProfileService(deps, input) {
621
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
622
+ const profileId = String(input.profileId || '').trim();
623
+ if (!profileId)
624
+ throw deps.createRuleError('profileId is required', 400, 'ARCHIVE_AI_PROFILE_INVALID_INPUT');
625
+ const existingRow = deps.db.prepare(`
626
+ SELECT *
627
+ FROM ai_profiles
628
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL
629
+ LIMIT 1
630
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
631
+ if (!existingRow) {
632
+ throw deps.createRuleError(`Profile ${profileId} not found`, 404, 'ARCHIVE_AI_PROFILE_NOT_FOUND');
633
+ }
634
+ const openTaskRow = deps.db.prepare(`
635
+ SELECT COUNT(*) AS c
636
+ FROM tasks
637
+ WHERE tenant_id = ?
638
+ AND workspace_id = ?
639
+ AND assignee = ?
640
+ AND COALESCE(is_archived, 0) = 0
641
+ AND LOWER(COALESCE(status, 'task')) NOT IN ('done', 'cancelled')
642
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
643
+ const openTaskCount = Math.max(0, Number(openTaskRow?.c || 0));
644
+ if (openTaskCount > 0) {
645
+ throw deps.createRuleError('This AI profile still owns open tasks. Reassign or close them before removing it from the roster.', 409, 'AI_PROFILE_HAS_OPEN_TASKS', { workspaceId: normalizedWorkspaceId, profileId, openTaskCount });
646
+ }
647
+ const archivedAt = new Date().toISOString();
648
+ deps.db.prepare(`
649
+ UPDATE ai_profiles
650
+ SET archived_at = ?,
651
+ archived_by = ?,
652
+ archived_reason = ?,
653
+ merged_into_profile_id = NULL,
654
+ roster_state_updated_at = ?,
655
+ updated_at = ?
656
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
657
+ `).run(archivedAt, normalizeNullableProfileReference(input.archivedBy), normalizeOptionalProfileField(input.reason) ?? 'manual_archive', archivedAt, archivedAt, deps.tenantId, normalizedWorkspaceId, profileId);
658
+ const archivedRow = deps.db.prepare(`
659
+ SELECT *
660
+ FROM ai_profiles
661
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
662
+ LIMIT 1
663
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
664
+ if (!archivedRow) {
665
+ throw deps.createRuleError('Failed to archive AI profile', 500, 'ARCHIVE_AI_PROFILE_FAILED');
666
+ }
667
+ return {
668
+ profile: hydrateAiProfileRecord(deps, archivedRow),
669
+ openTaskCount,
670
+ };
169
671
  }
170
- export function resolveAiProfileService(deps, input) {
672
+ export function resolveAiProfileService(deps, input, options) {
171
673
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
172
674
  const name = deps.normalizeAiProfileName(input.name);
173
675
  const profileToken = String(input.profileToken || '').trim();
174
676
  const now = new Date().toISOString();
677
+ const trustedSeatScope = normalizeAiProfileSeatScopeInput(deps, options?.trustedSeatScope);
175
678
  if (profileToken) {
176
- const existingRow = deps.db.prepare(`
679
+ const existingRows = deps.db.prepare(`
177
680
  SELECT *
178
681
  FROM ai_profiles
179
682
  WHERE tenant_id = ?
180
683
  AND workspace_id = ?
181
684
  AND profile_token = ?
182
- LIMIT 1
183
- `).get(deps.tenantId, normalizedWorkspaceId, profileToken);
685
+ AND archived_at IS NULL
686
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
687
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken);
688
+ const existingRow = findFirstAiProfileRowForTrustedSeatScope(deps, existingRows, trustedSeatScope);
184
689
  if (!existingRow) {
185
- throw deps.createRuleError('AI profile token is invalid for this workspace', 404, 'AI_PROFILE_NOT_FOUND');
690
+ const archivedRows = deps.db.prepare(`
691
+ SELECT *
692
+ FROM ai_profiles
693
+ WHERE tenant_id = ?
694
+ AND workspace_id = ?
695
+ AND profile_token = ?
696
+ AND archived_at IS NOT NULL
697
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
698
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken);
699
+ const archivedRow = findFirstAiProfileRowForTrustedSeatScope(deps, archivedRows, trustedSeatScope);
700
+ if (!archivedRow) {
701
+ if (existingRows.length === 0 && archivedRows.length === 0) {
702
+ throw deps.createRuleError('AI profile token is invalid for this workspace', 404, 'AI_PROFILE_NOT_FOUND');
703
+ }
704
+ }
705
+ else {
706
+ const existingProfile = deps.aiProfileRowToRecord(archivedRow);
707
+ const nextName = name;
708
+ const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
709
+ const nextIcon = input.icon === undefined
710
+ ? existingProfile.icon
711
+ : deps.normalizeAiProfileIcon(input.icon);
712
+ const nextColor = input.color === undefined
713
+ ? existingProfile.color
714
+ : deps.normalizeAiProfileColor(input.color);
715
+ const nextDescription = normalizeOptionalProfileField(input.description);
716
+ const nextRole = normalizeOptionalProfileField(input.role);
717
+ const nextProvider = normalizeOptionalProfileField(input.provider);
718
+ const nextModel = normalizeOptionalProfileField(input.model);
719
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
720
+ name: nextName,
721
+ description: nextDescription,
722
+ role: nextRole,
723
+ provider: nextProvider,
724
+ model: nextModel,
725
+ surfaceType: input.surfaceType,
726
+ }, existingProfile);
727
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
728
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
729
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
730
+ }
731
+ deps.db.prepare(`
732
+ UPDATE ai_profiles
733
+ SET name = ?,
734
+ username = ?,
735
+ icon = ?,
736
+ color = ?,
737
+ description = COALESCE(?, description),
738
+ role = COALESCE(?, role),
739
+ provider = COALESCE(?, provider),
740
+ model = COALESCE(?, model),
741
+ surface_type = CASE
742
+ WHEN ? = 1 THEN ?
743
+ ELSE surface_type
744
+ END,
745
+ seat_scope = CASE
746
+ WHEN ? = 1 THEN ?
747
+ ELSE seat_scope
748
+ END,
749
+ archived_at = NULL,
750
+ archived_by = NULL,
751
+ archived_reason = NULL,
752
+ merged_into_profile_id = NULL,
753
+ roster_state_updated_at = ?,
754
+ last_active_at = ?,
755
+ updated_at = ?
756
+ WHERE tenant_id = ? AND id = ?
757
+ `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, resolvedSeatScope.seatScopeProvided ? 1 : 0, resolvedSeatScope.seatScope, now, now, now, deps.tenantId, String(archivedRow.id || '').trim());
758
+ const refreshed = deps.db.prepare(`
759
+ SELECT *
760
+ FROM ai_profiles
761
+ WHERE tenant_id = ? AND id = ?
762
+ LIMIT 1
763
+ `).get(deps.tenantId, String(archivedRow.id || '').trim());
764
+ if (!refreshed) {
765
+ throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
766
+ }
767
+ return {
768
+ created: false,
769
+ profile: hydrateAiProfileRecord(deps, refreshed)
770
+ };
771
+ }
186
772
  }
187
- const existingProfile = deps.aiProfileRowToRecord(existingRow);
773
+ else {
774
+ const existingProfile = deps.aiProfileRowToRecord(existingRow);
775
+ const nextName = name;
776
+ const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
777
+ const nextIcon = input.icon === undefined
778
+ ? existingProfile.icon
779
+ : deps.normalizeAiProfileIcon(input.icon);
780
+ const nextColor = input.color === undefined
781
+ ? existingProfile.color
782
+ : deps.normalizeAiProfileColor(input.color);
783
+ const nextDescription = normalizeOptionalProfileField(input.description);
784
+ const nextRole = normalizeOptionalProfileField(input.role);
785
+ const nextProvider = normalizeOptionalProfileField(input.provider);
786
+ const nextModel = normalizeOptionalProfileField(input.model);
787
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
788
+ name: nextName,
789
+ description: nextDescription,
790
+ role: nextRole,
791
+ provider: nextProvider,
792
+ model: nextModel,
793
+ surfaceType: input.surfaceType,
794
+ }, existingProfile);
795
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
796
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
797
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
798
+ }
799
+ deps.db.prepare(`
800
+ UPDATE ai_profiles
801
+ SET name = ?,
802
+ username = ?,
803
+ icon = ?,
804
+ color = ?,
805
+ description = COALESCE(?, description),
806
+ role = COALESCE(?, role),
807
+ provider = COALESCE(?, provider),
808
+ model = COALESCE(?, model),
809
+ surface_type = CASE
810
+ WHEN ? = 1 THEN ?
811
+ ELSE surface_type
812
+ END,
813
+ seat_scope = CASE
814
+ WHEN ? = 1 THEN ?
815
+ ELSE seat_scope
816
+ END,
817
+ last_active_at = ?,
818
+ updated_at = ?
819
+ WHERE tenant_id = ? AND id = ?
820
+ `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, resolvedSeatScope.seatScopeProvided ? 1 : 0, resolvedSeatScope.seatScope, now, now, deps.tenantId, String(existingRow.id || '').trim());
821
+ const refreshed = deps.db.prepare(`
822
+ SELECT *
823
+ FROM ai_profiles
824
+ WHERE tenant_id = ? AND id = ?
825
+ LIMIT 1
826
+ `).get(deps.tenantId, String(existingRow.id || '').trim());
827
+ if (!refreshed) {
828
+ throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
829
+ }
830
+ return {
831
+ created: false,
832
+ profile: hydrateAiProfileRecord(deps, refreshed)
833
+ };
834
+ }
835
+ }
836
+ const exactMatches = deps.db.prepare(`
837
+ SELECT *
838
+ FROM ai_profiles
839
+ WHERE tenant_id = ?
840
+ AND workspace_id = ?
841
+ AND archived_at IS NULL
842
+ AND (
843
+ LOWER(name) = LOWER(?)
844
+ OR LOWER(username) = LOWER(?)
845
+ )
846
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
847
+ `).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
848
+ const scopedExactMatches = filterAiProfileRowsForTrustedSeatScope(deps, exactMatches, trustedSeatScope);
849
+ if (scopedExactMatches.length > 0) {
850
+ const canonicalId = String(scopedExactMatches[0].id || '').trim();
188
851
  const nextName = name;
189
852
  const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
190
- const nextIcon = input.icon === undefined
191
- ? existingProfile.icon
192
- : deps.normalizeAiProfileIcon(input.icon);
193
- const nextColor = input.color === undefined
194
- ? existingProfile.color
195
- : deps.normalizeAiProfileColor(input.color);
853
+ const nextIcon = deps.normalizeAiProfileIcon(input.icon);
854
+ const nextColor = deps.normalizeAiProfileColor(input.color);
196
855
  const nextDescription = normalizeOptionalProfileField(input.description);
197
856
  const nextRole = normalizeOptionalProfileField(input.role);
198
857
  const nextProvider = normalizeOptionalProfileField(input.provider);
199
858
  const nextModel = normalizeOptionalProfileField(input.model);
200
- const nextSurfaceType = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
859
+ const existingProfile = deps.aiProfileRowToRecord(scopedExactMatches[0]);
860
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
861
+ name: nextName,
862
+ description: nextDescription,
863
+ role: nextRole,
864
+ provider: nextProvider,
865
+ model: nextModel,
866
+ surfaceType: input.surfaceType,
867
+ }, existingProfile);
868
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
869
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
870
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
871
+ }
201
872
  deps.db.prepare(`
202
873
  UPDATE ai_profiles
203
874
  SET name = ?,
@@ -208,41 +879,53 @@ export function resolveAiProfileService(deps, input) {
208
879
  role = COALESCE(?, role),
209
880
  provider = COALESCE(?, provider),
210
881
  model = COALESCE(?, model),
882
+ last_active_at = ?,
883
+ updated_at = ?,
211
884
  surface_type = CASE
212
885
  WHEN ? = 1 THEN ?
213
886
  ELSE surface_type
214
887
  END,
215
- last_active_at = ?,
216
- updated_at = ?
888
+ seat_scope = CASE
889
+ WHEN ? = 1 THEN ?
890
+ ELSE seat_scope
891
+ END
217
892
  WHERE tenant_id = ? AND id = ?
218
- `).run(nextName, nextUsername, nextIcon, nextColor, nextDescription ?? null, nextRole ?? null, nextProvider ?? null, nextModel ?? null, nextSurfaceType !== undefined ? 1 : 0, nextSurfaceType, now, now, deps.tenantId, String(existingRow.id || '').trim());
219
- const refreshed = deps.db.prepare(`
893
+ `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, now, now, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, resolvedSeatScope.seatScopeProvided ? 1 : 0, resolvedSeatScope.seatScope, deps.tenantId, canonicalId);
894
+ const reused = deps.db.prepare(`
220
895
  SELECT *
221
896
  FROM ai_profiles
222
897
  WHERE tenant_id = ? AND id = ?
223
898
  LIMIT 1
224
- `).get(deps.tenantId, String(existingRow.id || '').trim());
225
- if (!refreshed) {
899
+ `).get(deps.tenantId, canonicalId);
900
+ if (!reused) {
226
901
  throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
227
902
  }
228
903
  return {
229
904
  created: false,
230
- profile: deps.aiProfileRowToRecord(refreshed)
905
+ profile: hydrateAiProfileRecord(deps, reused),
906
+ ...(scopedExactMatches.length > 1 ? {
907
+ duplicateWarning: {
908
+ existingProfileId: canonicalId,
909
+ message: `Multiple profiles matched "${name}". Reused ${canonicalId}; merge the remaining duplicates.`
910
+ }
911
+ } : {})
231
912
  };
232
913
  }
233
- const exactMatches = deps.db.prepare(`
914
+ const archivedMatches = deps.db.prepare(`
234
915
  SELECT *
235
916
  FROM ai_profiles
236
917
  WHERE tenant_id = ?
237
918
  AND workspace_id = ?
919
+ AND archived_at IS NOT NULL
238
920
  AND (
239
921
  LOWER(name) = LOWER(?)
240
922
  OR LOWER(username) = LOWER(?)
241
923
  )
242
924
  ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
243
925
  `).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
244
- if (exactMatches.length > 0) {
245
- const canonicalId = String(exactMatches[0].id || '').trim();
926
+ const scopedArchivedMatches = filterAiProfileRowsForTrustedSeatScope(deps, archivedMatches, trustedSeatScope);
927
+ if (scopedArchivedMatches.length > 0) {
928
+ const canonicalId = String(scopedArchivedMatches[0].id || '').trim();
246
929
  const nextName = name;
247
930
  const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
248
931
  const nextIcon = deps.normalizeAiProfileIcon(input.icon);
@@ -251,7 +934,19 @@ export function resolveAiProfileService(deps, input) {
251
934
  const nextRole = normalizeOptionalProfileField(input.role);
252
935
  const nextProvider = normalizeOptionalProfileField(input.provider);
253
936
  const nextModel = normalizeOptionalProfileField(input.model);
254
- const nextSurfaceType = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
937
+ const existingProfile = deps.aiProfileRowToRecord(scopedArchivedMatches[0]);
938
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
939
+ name: nextName,
940
+ description: nextDescription,
941
+ role: nextRole,
942
+ provider: nextProvider,
943
+ model: nextModel,
944
+ surfaceType: input.surfaceType,
945
+ }, existingProfile);
946
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
947
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
948
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
949
+ }
255
950
  deps.db.prepare(`
256
951
  UPDATE ai_profiles
257
952
  SET name = ?,
@@ -264,30 +959,33 @@ export function resolveAiProfileService(deps, input) {
264
959
  model = COALESCE(?, model),
265
960
  last_active_at = ?,
266
961
  updated_at = ?,
962
+ archived_at = NULL,
963
+ archived_by = NULL,
964
+ archived_reason = NULL,
965
+ merged_into_profile_id = NULL,
966
+ roster_state_updated_at = ?,
267
967
  surface_type = CASE
268
968
  WHEN ? = 1 THEN ?
269
969
  ELSE surface_type
970
+ END,
971
+ seat_scope = CASE
972
+ WHEN ? = 1 THEN ?
973
+ ELSE seat_scope
270
974
  END
271
975
  WHERE tenant_id = ? AND id = ?
272
- `).run(nextName, nextUsername, nextIcon, nextColor, nextDescription ?? null, nextRole ?? null, nextProvider ?? null, nextModel ?? null, now, now, nextSurfaceType !== undefined ? 1 : 0, nextSurfaceType, deps.tenantId, canonicalId);
273
- const reused = deps.db.prepare(`
976
+ `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, now, now, now, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, resolvedSeatScope.seatScopeProvided ? 1 : 0, resolvedSeatScope.seatScope, deps.tenantId, canonicalId);
977
+ const reactivated = deps.db.prepare(`
274
978
  SELECT *
275
979
  FROM ai_profiles
276
980
  WHERE tenant_id = ? AND id = ?
277
981
  LIMIT 1
278
982
  `).get(deps.tenantId, canonicalId);
279
- if (!reused) {
983
+ if (!reactivated) {
280
984
  throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
281
985
  }
282
986
  return {
283
987
  created: false,
284
- profile: deps.aiProfileRowToRecord(reused),
285
- ...(exactMatches.length > 1 ? {
286
- duplicateWarning: {
287
- existingProfileId: canonicalId,
288
- message: `Multiple profiles matched "${name}". Reused ${canonicalId}; merge the remaining duplicates.`
289
- }
290
- } : {})
988
+ profile: hydrateAiProfileRecord(deps, reactivated)
291
989
  };
292
990
  }
293
991
  const id = deps.generateEntityId('ai-profile');
@@ -299,16 +997,29 @@ export function resolveAiProfileService(deps, input) {
299
997
  const role = normalizeOptionalProfileField(input.role);
300
998
  const provider = normalizeOptionalProfileField(input.provider);
301
999
  const model = normalizeOptionalProfileField(input.model);
302
- const surfaceType = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
1000
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
1001
+ name,
1002
+ description,
1003
+ role,
1004
+ provider,
1005
+ model,
1006
+ surfaceType: input.surfaceType,
1007
+ }, null);
1008
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, null);
1009
+ if (resolvedSeatScope.seatScope === 'cloud_metered') {
1010
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1011
+ }
303
1012
  const providerMetadata = input.providerMetadata && typeof input.providerMetadata === 'object'
304
1013
  ? input.providerMetadata
305
1014
  : null;
306
1015
  deps.db.prepare(`
307
1016
  INSERT INTO ai_profiles (
308
1017
  id, tenant_id, workspace_id, profile_token, name, username, icon, color,
309
- description, role, provider, model, surface_type, provider_metadata_json, created_at, updated_at, last_active_at
310
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
311
- `).run(id, deps.tenantId, normalizedWorkspaceId, issuedProfileToken, name, username, icon, color, description ?? null, role ?? null, provider ?? null, model ?? null, surfaceType, providerMetadata ? JSON.stringify(providerMetadata) : null, now, now, now);
1018
+ avatar_url, avatar_source_url, description, role, provider, model, surface_type, seat_scope, provider_metadata_json,
1019
+ archived_at, archived_by, archived_reason, merged_into_profile_id,
1020
+ roster_state_updated_at, created_at, updated_at, last_active_at
1021
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1022
+ `).run(id, deps.tenantId, normalizedWorkspaceId, issuedProfileToken, name, username, icon, color, null, null, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceType, resolvedSeatScope.seatScope, providerMetadata ? JSON.stringify(providerMetadata) : null, null, null, null, null, now, now, now, now);
312
1023
  const created = deps.db.prepare(`
313
1024
  SELECT *
314
1025
  FROM ai_profiles
@@ -320,6 +1031,6 @@ export function resolveAiProfileService(deps, input) {
320
1031
  }
321
1032
  return {
322
1033
  created: true,
323
- profile: deps.aiProfileRowToRecord(created),
1034
+ profile: hydrateAiProfileRecord(deps, created),
324
1035
  };
325
1036
  }