@taskforcehq/taskforce 0.3.305 → 0.3.307

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 (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  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 +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. 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,22 +19,278 @@ 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 normalizeAiProfileAvatarRevision(value) {
26
+ if (value === undefined || value === null || value === '')
27
+ return 0;
28
+ const normalized = typeof value === 'number' ? value : Number(value);
29
+ if (!Number.isFinite(normalized))
30
+ return 0;
31
+ return Math.max(0, Math.floor(normalized));
32
+ }
33
+ function hasAiProfileAvatarState(avatarUrl, avatarSourceUrl) {
34
+ return Boolean(avatarUrl || avatarSourceUrl);
35
+ }
36
+ function compareIsoStrings(left, right) {
37
+ const normalizedLeft = normalizeNullableIsoString(left);
38
+ const normalizedRight = normalizeNullableIsoString(right);
39
+ if (!normalizedLeft && !normalizedRight)
40
+ return 0;
41
+ if (!normalizedLeft)
42
+ return -1;
43
+ if (!normalizedRight)
44
+ return 1;
45
+ return normalizedLeft.localeCompare(normalizedRight);
46
+ }
47
+ function deriveRosterStateUpdatedAt(value, fallbacks) {
48
+ const explicitValue = normalizeNullableIsoString(value);
49
+ const archivedAt = normalizeNullableIsoString(fallbacks.archivedAt);
50
+ const createdAt = normalizeNullableIsoString(fallbacks.createdAt);
51
+ if (archivedAt) {
52
+ return explicitValue && compareIsoStrings(explicitValue, archivedAt) >= 0
53
+ ? explicitValue
54
+ : archivedAt;
55
+ }
56
+ // Legacy active snapshots may carry fresh metadata updatedAt values without
57
+ // carrying an intentional roster lifecycle event. Treat creation as the
58
+ // active lifecycle baseline so metadata-only sync cannot resurrect archives.
59
+ return explicitValue ?? createdAt;
60
+ }
10
61
  function normalizeAiProfileSurfaceTypeInput(deps, value) {
11
62
  if (value === undefined)
12
63
  return undefined;
13
64
  if (value === null)
14
65
  return null;
15
66
  if (typeof value !== 'string') {
16
- throw deps.createRuleError('AI profile surfaceType must be one of chat_app, ide, coding_tool, agent, or other', 400, 'AI_PROFILE_SURFACE_TYPE_INVALID');
67
+ throw deps.createRuleError('AI profile surfaceType must be one of chat_app, ide, cli, coding_tool, or agent', 400, 'AI_PROFILE_SURFACE_TYPE_INVALID');
17
68
  }
18
69
  const normalized = value.trim();
19
70
  if (!normalized)
20
71
  return null;
72
+ if (normalized === 'other')
73
+ return 'agent';
21
74
  if (!isAiProfileSurfaceType(normalized)) {
22
- throw deps.createRuleError('AI profile surfaceType must be one of chat_app, ide, coding_tool, agent, or other', 400, 'AI_PROFILE_SURFACE_TYPE_INVALID');
75
+ throw deps.createRuleError('AI profile surfaceType must be one of chat_app, ide, cli, coding_tool, or agent', 400, 'AI_PROFILE_SURFACE_TYPE_INVALID');
23
76
  }
24
77
  return normalized;
25
78
  }
79
+ function readAiProfileProviderMetadataJson(value) {
80
+ if (typeof value !== 'string' || value.trim().length === 0)
81
+ return null;
82
+ try {
83
+ const parsed = JSON.parse(value);
84
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
85
+ ? parsed
86
+ : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ function hasManualSurfaceTypeOverride(profile) {
93
+ const taskforceMetadata = profile?.providerMetadata?.taskforce;
94
+ return Boolean(taskforceMetadata
95
+ && typeof taskforceMetadata === 'object'
96
+ && !Array.isArray(taskforceMetadata)
97
+ && taskforceMetadata.surfaceTypeManuallySet === true);
98
+ }
99
+ function markManualSurfaceTypeOverride(row) {
100
+ const metadata = readAiProfileProviderMetadataJson(row.provider_metadata_json) ?? {};
101
+ const taskforceMetadata = metadata.taskforce && typeof metadata.taskforce === 'object' && !Array.isArray(metadata.taskforce)
102
+ ? metadata.taskforce
103
+ : {};
104
+ return JSON.stringify({
105
+ ...metadata,
106
+ taskforce: {
107
+ ...taskforceMetadata,
108
+ surfaceTypeManuallySet: true
109
+ }
110
+ });
111
+ }
112
+ function clearManualSurfaceTypeOverride(row) {
113
+ const metadata = readAiProfileProviderMetadataJson(row.provider_metadata_json);
114
+ if (!metadata)
115
+ return null;
116
+ const taskforceMetadata = metadata.taskforce && typeof metadata.taskforce === 'object' && !Array.isArray(metadata.taskforce)
117
+ ? { ...metadata.taskforce }
118
+ : null;
119
+ if (!taskforceMetadata || taskforceMetadata.surfaceTypeManuallySet !== true) {
120
+ return JSON.stringify(metadata);
121
+ }
122
+ delete taskforceMetadata.surfaceTypeManuallySet;
123
+ const nextMetadata = { ...metadata };
124
+ if (Object.keys(taskforceMetadata).length > 0) {
125
+ nextMetadata.taskforce = taskforceMetadata;
126
+ }
127
+ else {
128
+ delete nextMetadata.taskforce;
129
+ }
130
+ return Object.keys(nextMetadata).length > 0
131
+ ? JSON.stringify(nextMetadata)
132
+ : null;
133
+ }
134
+ function normalizeAiProfileSeatScopeInput(deps, value) {
135
+ if (value === undefined)
136
+ return undefined;
137
+ if (value === null)
138
+ return null;
139
+ if (typeof value !== 'string') {
140
+ throw deps.createRuleError('AI profile seatScope must be cloud_metered or local_unmetered', 400, 'AI_PROFILE_SEAT_SCOPE_INVALID');
141
+ }
142
+ const normalized = value.trim();
143
+ if (!normalized)
144
+ return null;
145
+ if (!isAiProfileSeatScope(normalized)) {
146
+ throw deps.createRuleError('AI profile seatScope must be cloud_metered or local_unmetered', 400, 'AI_PROFILE_SEAT_SCOPE_INVALID');
147
+ }
148
+ return normalized;
149
+ }
150
+ function resolveAiProfileSeatScope(deps, inputSeatScope, existing) {
151
+ const incomingSeatScope = normalizeAiProfileSeatScopeInput(deps, inputSeatScope);
152
+ const existingSeatScope = existing?.seatScope ?? null;
153
+ if (incomingSeatScope === undefined) {
154
+ return {
155
+ seatScope: existingSeatScope,
156
+ seatScopeProvided: false,
157
+ };
158
+ }
159
+ if (incomingSeatScope === 'cloud_metered' || existingSeatScope === 'cloud_metered') {
160
+ return {
161
+ seatScope: 'cloud_metered',
162
+ seatScopeProvided: true,
163
+ };
164
+ }
165
+ return {
166
+ seatScope: incomingSeatScope ?? existingSeatScope,
167
+ seatScopeProvided: true,
168
+ };
169
+ }
170
+ function resolveSyncedAiProfileSeatScope(deps, inputSeatScope, existing) {
171
+ const incomingSeatScope = normalizeAiProfileSeatScopeInput(deps, inputSeatScope);
172
+ const existingSeatScope = existing?.seatScope ?? null;
173
+ if (incomingSeatScope === undefined) {
174
+ return {
175
+ seatScope: existingSeatScope,
176
+ seatScopeProvided: false,
177
+ };
178
+ }
179
+ return {
180
+ seatScope: incomingSeatScope ?? existingSeatScope,
181
+ seatScopeProvided: true,
182
+ };
183
+ }
184
+ function resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existing) {
185
+ const normalizedTrustedSeatScope = normalizeAiProfileSeatScopeInput(deps, trustedSeatScope);
186
+ if (normalizedTrustedSeatScope === undefined) {
187
+ return {
188
+ seatScope: existing?.seatScope ?? null,
189
+ seatScopeProvided: false,
190
+ };
191
+ }
192
+ return {
193
+ seatScope: normalizedTrustedSeatScope,
194
+ seatScopeProvided: true,
195
+ };
196
+ }
197
+ function aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope) {
198
+ if (trustedSeatScope === undefined)
199
+ return true;
200
+ const profile = deps.aiProfileRowToRecord(row);
201
+ const profileSeatScope = profile.seatScope ?? null;
202
+ if (profileSeatScope === null)
203
+ return true;
204
+ return profileSeatScope === trustedSeatScope;
205
+ }
206
+ function findFirstAiProfileRowForTrustedSeatScope(deps, rows, trustedSeatScope) {
207
+ if (trustedSeatScope === undefined)
208
+ return rows[0];
209
+ return rows.find((row) => aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope));
210
+ }
211
+ function filterAiProfileRowsForTrustedSeatScope(deps, rows, trustedSeatScope) {
212
+ if (trustedSeatScope === undefined)
213
+ return rows;
214
+ return rows.filter((row) => aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope));
215
+ }
216
+ function normalizeAiProfileIdentityValue(value) {
217
+ return String(value || '').trim().toLowerCase();
218
+ }
219
+ function latestAiProfileActivityValue(row) {
220
+ return normalizeNullableIsoString(row.last_active_at)
221
+ ?? normalizeNullableIsoString(row.updated_at)
222
+ ?? normalizeNullableIsoString(row.created_at)
223
+ ?? '';
224
+ }
225
+ function rankNullableIdentityMatch(existingValue, incomingValue) {
226
+ const existing = normalizeAiProfileIdentityValue(existingValue);
227
+ if (incomingValue) {
228
+ if (existing === incomingValue)
229
+ return 2;
230
+ if (!existing)
231
+ return 1;
232
+ return 0;
233
+ }
234
+ return existing ? 0 : 1;
235
+ }
236
+ function selectAiProfileIdentityMatch(deps, rows, input, trustedSeatScope) {
237
+ const scopedRows = filterAiProfileRowsForTrustedSeatScope(deps, rows, trustedSeatScope);
238
+ const incomingProvider = normalizeAiProfileIdentityValue(input.provider) || null;
239
+ const incomingModel = normalizeAiProfileIdentityValue(input.model) || null;
240
+ const incomingSurfaceType = input.surfaceTypeProvided
241
+ ? normalizeAiProfileIdentityValue(input.surfaceType) || null
242
+ : null;
243
+ const ranked = scopedRows
244
+ .map((row) => {
245
+ const providerRank = rankNullableIdentityMatch(row.provider, incomingProvider);
246
+ const surfaceRank = input.surfaceTypeProvided
247
+ ? rankNullableIdentityMatch(row.surface_type, incomingSurfaceType)
248
+ : 0;
249
+ const modelRank = incomingModel && normalizeAiProfileIdentityValue(row.model) === incomingModel ? 1 : 0;
250
+ return {
251
+ row,
252
+ providerRank,
253
+ surfaceRank,
254
+ modelRank,
255
+ activity: latestAiProfileActivityValue(row),
256
+ };
257
+ })
258
+ .filter((entry) => Boolean(entry))
259
+ .sort((left, right) => {
260
+ if (right.providerRank !== left.providerRank)
261
+ return right.providerRank - left.providerRank;
262
+ if (right.surfaceRank !== left.surfaceRank)
263
+ return right.surfaceRank - left.surfaceRank;
264
+ if (right.modelRank !== left.modelRank)
265
+ return right.modelRank - left.modelRank;
266
+ const activityDelta = right.activity.localeCompare(left.activity);
267
+ if (activityDelta !== 0)
268
+ return activityDelta;
269
+ return String(left.row.id || '').localeCompare(String(right.row.id || ''));
270
+ });
271
+ return {
272
+ row: ranked[0]?.row,
273
+ matchCount: ranked.length,
274
+ };
275
+ }
276
+ function requiresCloudSeatReservation(existing, nextSeatScope) {
277
+ if (nextSeatScope !== 'cloud_metered')
278
+ return false;
279
+ if (existing?.archivedAt)
280
+ return true;
281
+ return existing?.seatScope !== 'cloud_metered';
282
+ }
283
+ function assertAiProfileSeatAvailable(deps, workspaceId) {
284
+ const aiProfileUsage = deps.countAiProfileUsage(workspaceId);
285
+ if (aiProfileUsage.limit !== null && aiProfileUsage.used >= aiProfileUsage.limit) {
286
+ 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', {
287
+ workspaceId,
288
+ limit: aiProfileUsage.limit,
289
+ used: aiProfileUsage.used,
290
+ remaining: aiProfileUsage.remaining
291
+ });
292
+ }
293
+ }
26
294
  function inferAiProfileProviderModelDefaults(name) {
27
295
  const normalized = String(name || '').trim().toLowerCase();
28
296
  if (!normalized)
@@ -54,6 +322,12 @@ function inferAiProfileMetadataDefaults(name, surfaceType) {
54
322
  role: 'Coding Agent',
55
323
  ...inferredProviderModel
56
324
  };
325
+ case 'cli':
326
+ return {
327
+ description: `${name} CLI coding agent connected through MCP.`,
328
+ role: 'CLI Coding Agent',
329
+ ...inferredProviderModel
330
+ };
57
331
  case 'chat_app':
58
332
  return {
59
333
  description: `${name} conversational AI connected through Taskforce.`,
@@ -66,12 +340,6 @@ function inferAiProfileMetadataDefaults(name, surfaceType) {
66
340
  role: 'Coding Assistant',
67
341
  ...inferredProviderModel
68
342
  };
69
- case 'other':
70
- return {
71
- description: `${name} AI profile connected through Taskforce.`,
72
- role: 'AI Assistant',
73
- ...inferredProviderModel
74
- };
75
343
  default:
76
344
  return {};
77
345
  }
@@ -87,7 +355,9 @@ function resolveOptionalProfileField(inputValue, existingValue, fallbackValue) {
87
355
  }
88
356
  function resolveAiProfileMetadataFields(deps, input, existing) {
89
357
  const surfaceTypeInput = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
358
+ const manuallyOverridden = hasManualSurfaceTypeOverride(existing);
90
359
  const effectiveSurfaceType = surfaceTypeInput === undefined
360
+ || manuallyOverridden
91
361
  ? existing?.surfaceType ?? null
92
362
  : surfaceTypeInput;
93
363
  const inferredDefaults = inferAiProfileMetadataDefaults(input.name, effectiveSurfaceType);
@@ -97,7 +367,7 @@ function resolveAiProfileMetadataFields(deps, input, existing) {
97
367
  provider: resolveOptionalProfileField(input.provider, existing?.provider, inferredDefaults.provider) ?? null,
98
368
  model: resolveOptionalProfileField(input.model, existing?.model, inferredDefaults.model) ?? null,
99
369
  surfaceType: effectiveSurfaceType,
100
- surfaceTypeProvided: surfaceTypeInput !== undefined,
370
+ surfaceTypeProvided: surfaceTypeInput !== undefined && !manuallyOverridden,
101
371
  };
102
372
  }
103
373
  function applyAiProfileMetadataDefaults(record) {
@@ -110,7 +380,167 @@ function applyAiProfileMetadataDefaults(record) {
110
380
  model: resolveOptionalProfileField(undefined, record.model, inferredDefaults.model) ?? null,
111
381
  };
112
382
  }
113
- function hydrateAiProfileRecord(deps, row) {
383
+ function backfillMissingAiProfileAvatarFromDuplicate(deps, row) {
384
+ const id = String(row.id || '').trim();
385
+ const workspaceId = String(row.workspace_id || '').trim();
386
+ const name = String(row.name || '').trim();
387
+ const username = String(row.username || '').trim();
388
+ if (!id || !workspaceId || (!name && !username))
389
+ return;
390
+ const currentAvatarUrl = normalizeAiProfileAvatarUrl(row.avatar_url);
391
+ const currentAvatarSourceUrl = normalizeAiProfileAvatarUrl(row.avatar_source_url);
392
+ if (hasAiProfileAvatarState(currentAvatarUrl, currentAvatarSourceUrl))
393
+ return;
394
+ const currentRevision = normalizeAiProfileAvatarRevision(row.avatar_revision);
395
+ const donor = deps.db.prepare(`
396
+ SELECT avatar_url, avatar_source_url, avatar_revision, avatar_updated_at, updated_at
397
+ FROM ai_profiles
398
+ WHERE tenant_id = ?
399
+ AND workspace_id = ?
400
+ AND id <> ?
401
+ AND archived_at IS NULL
402
+ AND (
403
+ (? <> '' AND LOWER(name) = LOWER(?))
404
+ OR (? <> '' AND LOWER(username) = LOWER(?))
405
+ )
406
+ AND (
407
+ (avatar_url IS NOT NULL AND TRIM(avatar_url) <> '')
408
+ OR (avatar_source_url IS NOT NULL AND TRIM(avatar_source_url) <> '')
409
+ )
410
+ ORDER BY COALESCE(avatar_revision, 0) DESC,
411
+ COALESCE(avatar_updated_at, updated_at) DESC,
412
+ id ASC
413
+ LIMIT 1
414
+ `).get(deps.tenantId, workspaceId, id, name, name, username, username);
415
+ if (!donor)
416
+ return;
417
+ const donorRevision = normalizeAiProfileAvatarRevision(donor.avatar_revision);
418
+ if (donorRevision < currentRevision)
419
+ return;
420
+ const donorAvatarUrl = normalizeAiProfileAvatarUrl(donor.avatar_url);
421
+ const donorAvatarSourceUrl = normalizeAiProfileAvatarUrl(donor.avatar_source_url);
422
+ if (!hasAiProfileAvatarState(donorAvatarUrl, donorAvatarSourceUrl))
423
+ return;
424
+ const nextRevision = Math.max(currentRevision, donorRevision, 1);
425
+ const nextAvatarUpdatedAt = normalizeNullableIsoString(donor.avatar_updated_at)
426
+ ?? normalizeNullableIsoString(donor.updated_at)
427
+ ?? new Date().toISOString();
428
+ deps.db.prepare(`
429
+ UPDATE ai_profiles
430
+ SET avatar_url = ?,
431
+ avatar_source_url = ?,
432
+ avatar_revision = ?,
433
+ avatar_updated_at = ?,
434
+ updated_at = ?
435
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
436
+ `).run(donorAvatarUrl, donorAvatarSourceUrl, nextRevision, nextAvatarUpdatedAt, nextAvatarUpdatedAt, deps.tenantId, workspaceId, id);
437
+ row.avatar_url = donorAvatarUrl;
438
+ row.avatar_source_url = donorAvatarSourceUrl;
439
+ row.avatar_revision = nextRevision;
440
+ row.avatar_updated_at = nextAvatarUpdatedAt;
441
+ row.updated_at = nextAvatarUpdatedAt;
442
+ }
443
+ function backfillMissingAiProfileAvatarFromAssetIndex(deps, row) {
444
+ const id = String(row.id || '').trim();
445
+ const workspaceId = String(row.workspace_id || '').trim();
446
+ if (!id || !workspaceId)
447
+ return;
448
+ const currentAvatarUrl = normalizeAiProfileAvatarUrl(row.avatar_url);
449
+ const currentAvatarSourceUrl = normalizeAiProfileAvatarUrl(row.avatar_source_url);
450
+ if (hasAiProfileAvatarState(currentAvatarUrl, currentAvatarSourceUrl))
451
+ return;
452
+ if (normalizeAiProfileAvatarRevision(row.avatar_revision) > 0)
453
+ return;
454
+ if (normalizeNullableIsoString(row.avatar_updated_at))
455
+ return;
456
+ const avatarPrefix = `workspaces/${workspaceId}/assets/images/ai-profiles/${id}/avatar/`;
457
+ try {
458
+ const latestCropped = deps.db.prepare(`
459
+ SELECT storage_key, updated_at, created_at
460
+ FROM workspace_assets
461
+ WHERE tenant_id = ?
462
+ AND workspace_id = ?
463
+ AND deleted_at IS NULL
464
+ AND storage_key LIKE ?
465
+ AND (
466
+ LOWER(original_filename) LIKE 'cropped-%'
467
+ OR LOWER(logical_name) = 'ai profile avatar display'
468
+ OR LOWER(storage_key) LIKE '%/display.webp'
469
+ OR LOWER(storage_key) LIKE '%/display.gif'
470
+ OR LOWER(storage_key) LIKE '%/display.png'
471
+ OR LOWER(storage_key) LIKE '%/display.jpg'
472
+ OR LOWER(storage_key) LIKE '%/display.jpeg'
473
+ OR LOWER(original_filename) = 'avatar.webp'
474
+ OR LOWER(original_filename) = 'avatar.png'
475
+ OR LOWER(original_filename) = 'avatar.jpg'
476
+ OR LOWER(original_filename) = 'avatar.jpeg'
477
+ )
478
+ ORDER BY COALESCE(updated_at, created_at) DESC, storage_key DESC
479
+ LIMIT 1
480
+ `).get(deps.tenantId, workspaceId, `${avatarPrefix}%`);
481
+ if (!latestCropped)
482
+ return;
483
+ const latestSource = deps.db.prepare(`
484
+ SELECT storage_key, updated_at, created_at
485
+ FROM workspace_assets
486
+ WHERE tenant_id = ?
487
+ AND workspace_id = ?
488
+ AND deleted_at IS NULL
489
+ AND storage_key LIKE ?
490
+ AND (
491
+ LOWER(original_filename) LIKE 'source-%'
492
+ OR LOWER(logical_name) = 'ai profile avatar source'
493
+ OR LOWER(storage_key) LIKE '%/source.webp'
494
+ OR LOWER(storage_key) LIKE '%/source.gif'
495
+ OR LOWER(storage_key) LIKE '%/source.png'
496
+ OR LOWER(storage_key) LIKE '%/source.jpg'
497
+ OR LOWER(storage_key) LIKE '%/source.jpeg'
498
+ )
499
+ ORDER BY COALESCE(updated_at, created_at) DESC, storage_key DESC
500
+ LIMIT 1
501
+ `).get(deps.tenantId, workspaceId, `${avatarPrefix}%`);
502
+ const avatarStorageKey = String(latestCropped.storage_key || '').trim();
503
+ if (!avatarStorageKey)
504
+ return;
505
+ const sourceStorageKey = String(latestSource?.storage_key || '').trim();
506
+ const avatarUrl = `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`;
507
+ const avatarSourceUrl = sourceStorageKey
508
+ ? `/api/taskforce/context/${encodeURIComponent(sourceStorageKey)}`
509
+ : null;
510
+ const repairedAt = normalizeNullableIsoString(latestCropped.updated_at)
511
+ ?? normalizeNullableIsoString(latestCropped.created_at)
512
+ ?? new Date().toISOString();
513
+ deps.db.prepare(`
514
+ UPDATE ai_profiles
515
+ SET avatar_url = ?,
516
+ avatar_source_url = ?,
517
+ avatar_revision = 1,
518
+ avatar_updated_at = ?,
519
+ updated_at = CASE
520
+ WHEN updated_at IS NULL OR TRIM(updated_at) = '' OR updated_at < ? THEN ?
521
+ ELSE updated_at
522
+ END
523
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
524
+ AND (avatar_url IS NULL OR TRIM(avatar_url) = '')
525
+ AND (avatar_source_url IS NULL OR TRIM(avatar_source_url) = '')
526
+ AND COALESCE(avatar_revision, 0) = 0
527
+ AND (avatar_updated_at IS NULL OR TRIM(avatar_updated_at) = '')
528
+ `).run(avatarUrl, avatarSourceUrl, repairedAt, repairedAt, repairedAt, deps.tenantId, workspaceId, id);
529
+ row.avatar_url = avatarUrl;
530
+ row.avatar_source_url = avatarSourceUrl;
531
+ row.avatar_revision = 1;
532
+ row.avatar_updated_at = repairedAt;
533
+ if (!normalizeNullableIsoString(row.updated_at) || String(row.updated_at) < repairedAt) {
534
+ row.updated_at = repairedAt;
535
+ }
536
+ }
537
+ catch {
538
+ // Older or mocked stores may not have workspace_assets available.
539
+ }
540
+ }
541
+ function hydrateAiProfileRecord(deps, row, diagnostics) {
542
+ backfillMissingAiProfileAvatarFromDuplicate(deps, row);
543
+ backfillMissingAiProfileAvatarFromAssetIndex(deps, row);
114
544
  const record = deps.aiProfileRowToRecord(row);
115
545
  const hydrated = applyAiProfileMetadataDefaults(record);
116
546
  const metadataChanged = record.description !== hydrated.description
@@ -126,11 +556,15 @@ function hydrateAiProfileRecord(deps, row) {
126
556
  model = ?
127
557
  WHERE tenant_id = ? AND id = ?
128
558
  `).run(hydrated.description ?? null, hydrated.role ?? null, hydrated.provider ?? null, hydrated.model ?? null, deps.tenantId, record.id);
559
+ if (diagnostics) {
560
+ diagnostics.metadataBackfillCount = (diagnostics.metadataBackfillCount || 0) + 1;
561
+ }
129
562
  }
130
563
  return hydrated;
131
564
  }
132
- export function listAiProfilesService(deps, workspaceId) {
565
+ export function listAiProfilesService(deps, workspaceId, options) {
133
566
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
567
+ const includeArchived = options?.includeArchived === true;
134
568
  const rows = deps.db.prepare(`
135
569
  SELECT
136
570
  id,
@@ -140,35 +574,50 @@ export function listAiProfilesService(deps, workspaceId) {
140
574
  username,
141
575
  icon,
142
576
  color,
577
+ avatar_url,
578
+ avatar_source_url,
579
+ avatar_revision,
580
+ avatar_updated_at,
143
581
  description,
144
582
  role,
145
583
  provider,
146
584
  model,
147
585
  surface_type,
586
+ seat_scope,
148
587
  provider_metadata_json,
588
+ archived_at,
589
+ archived_by,
590
+ archived_reason,
591
+ merged_into_profile_id,
592
+ roster_state_updated_at,
149
593
  created_at,
150
594
  updated_at,
151
595
  last_active_at
152
596
  FROM ai_profiles
153
597
  WHERE tenant_id = ?
154
598
  AND workspace_id = ?
599
+ AND (? = 1 OR archived_at IS NULL)
155
600
  ORDER BY COALESCE(updated_at, created_at) DESC, LOWER(name) ASC
156
- `).all(deps.tenantId, normalizedWorkspaceId);
157
- return rows.map((row) => hydrateAiProfileRecord(deps, row));
601
+ `).all(deps.tenantId, normalizedWorkspaceId, includeArchived ? 1 : 0);
602
+ return rows.map((row) => hydrateAiProfileRecord(deps, row, options?.diagnostics));
158
603
  }
159
604
  export function getAiProfileByTokenService(deps, input) {
160
605
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
161
606
  const profileToken = String(input.profileToken || '').trim();
607
+ const includeArchived = input.includeArchived === true;
608
+ const trustedSeatScope = normalizeAiProfileSeatScopeInput(deps, input.seatScope);
162
609
  if (!profileToken)
163
610
  return null;
164
- const row = deps.db.prepare(`
611
+ const rows = deps.db.prepare(`
165
612
  SELECT *
166
613
  FROM ai_profiles
167
614
  WHERE tenant_id = ?
168
615
  AND workspace_id = ?
169
616
  AND profile_token = ?
170
- LIMIT 1
171
- `).get(deps.tenantId, normalizedWorkspaceId, profileToken);
617
+ AND (? = 1 OR archived_at IS NULL)
618
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
619
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken, includeArchived ? 1 : 0);
620
+ const row = findFirstAiProfileRowForTrustedSeatScope(deps, rows, trustedSeatScope);
172
621
  return row ? hydrateAiProfileRecord(deps, row) : null;
173
622
  }
174
623
  export function upsertAiProfileForSyncService(deps, profile) {
@@ -183,32 +632,184 @@ export function upsertAiProfileForSyncService(deps, profile) {
183
632
  const normalizedLastActiveAt = typeof profile.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
184
633
  ? profile.lastActiveAt.trim()
185
634
  : null;
635
+ const normalizedArchivedAt = normalizeNullableIsoString(profile.archivedAt);
636
+ const normalizedArchivedBy = normalizeNullableProfileReference(profile.archivedBy);
637
+ const normalizedArchivedReason = normalizeOptionalProfileField(profile.archivedReason) ?? null;
638
+ const normalizedMergedIntoProfileId = normalizeNullableProfileReference(profile.mergedIntoProfileId);
639
+ const normalizedRosterStateUpdatedAt = deriveRosterStateUpdatedAt(profile.rosterStateUpdatedAt, {
640
+ archivedAt: normalizedArchivedAt,
641
+ createdAt: normalizedCreatedAt,
642
+ updatedAt: normalizedUpdatedAt,
643
+ });
186
644
  const existingRow = deps.db.prepare(`
187
645
  SELECT *
188
646
  FROM ai_profiles
189
647
  WHERE tenant_id = ? AND workspace_id = ? AND id = ?
190
648
  LIMIT 1
191
649
  `).get(deps.tenantId, normalizedWorkspaceId, profileId);
192
- if (existingRow) {
193
- const existingUpdatedAt = String(existingRow.updated_at || existingRow.created_at || '').trim();
194
- if (existingUpdatedAt && existingUpdatedAt > normalizedUpdatedAt) {
195
- return hydrateAiProfileRecord(deps, existingRow);
196
- }
197
- }
198
650
  const existingProfile = existingRow ? deps.aiProfileRowToRecord(existingRow) : null;
651
+ const incomingAvatarUrlProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarUrl');
652
+ const incomingAvatarSourceUrlProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarSourceUrl');
653
+ const incomingAvatarRevisionProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarRevision');
654
+ const incomingAvatarRevision = normalizeAiProfileAvatarRevision(profile.avatarRevision);
655
+ const incomingAvatarUpdatedAt = normalizeNullableIsoString(profile.avatarUpdatedAt);
656
+ const existingCreatedAt = existingRow
657
+ ? String(existingRow.created_at || existingRow.updated_at || '').trim()
658
+ : '';
659
+ const existingUpdatedAt = existingRow
660
+ ? String(existingRow.updated_at || existingRow.created_at || '').trim()
661
+ : '';
662
+ const existingLastActiveAt = existingRow
663
+ ? normalizeNullableIsoString(existingRow.last_active_at)
664
+ : null;
665
+ const existingArchivedAt = existingRow
666
+ ? normalizeNullableIsoString(existingRow.archived_at)
667
+ : null;
668
+ const existingArchivedBy = existingRow
669
+ ? normalizeNullableProfileReference(existingRow.archived_by)
670
+ : null;
671
+ const existingArchivedReason = existingRow
672
+ ? normalizeOptionalProfileField(existingRow.archived_reason) ?? null
673
+ : null;
674
+ const existingMergedIntoProfileId = existingRow
675
+ ? normalizeNullableProfileReference(existingRow.merged_into_profile_id)
676
+ : null;
677
+ const existingRosterStateUpdatedAt = existingRow
678
+ ? deriveRosterStateUpdatedAt(existingRow.roster_state_updated_at, {
679
+ archivedAt: existingArchivedAt,
680
+ createdAt: existingCreatedAt,
681
+ updatedAt: existingUpdatedAt,
682
+ })
683
+ : null;
684
+ const existingAvatarRevision = normalizeAiProfileAvatarRevision(existingRow?.avatar_revision);
685
+ const existingAvatarUpdatedAt = normalizeNullableIsoString(existingRow?.avatar_updated_at);
686
+ const metadataComparison = compareIsoStrings(normalizedUpdatedAt, existingUpdatedAt);
687
+ const incomingMetadataWins = !existingRow || metadataComparison > 0;
688
+ const lifecycleComparison = compareIsoStrings(normalizedRosterStateUpdatedAt, existingRosterStateUpdatedAt);
689
+ const incomingArchived = normalizedArchivedAt !== null;
690
+ const existingArchived = existingArchivedAt !== null;
691
+ const incomingLifecycleWins = !existingRow
692
+ || lifecycleComparison > 0
693
+ || (lifecycleComparison === 0 && incomingArchived && !existingArchived);
694
+ const metadataSourceProfile = incomingMetadataWins
695
+ ? {
696
+ id: profileId,
697
+ workspaceId: normalizedWorkspaceId,
698
+ profileToken: String(profile.profileToken || '').trim(),
699
+ name: deps.normalizeAiProfileName(profile.name),
700
+ username: deps.normalizeAiProfileUsername(profile.username, profile.name),
701
+ icon: deps.normalizeAiProfileIcon(profile.icon),
702
+ color: deps.normalizeAiProfileColor(profile.color),
703
+ avatarUrl: incomingAvatarUrlProvided
704
+ ? normalizeAiProfileAvatarUrl(profile.avatarUrl)
705
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarUrl),
706
+ avatarSourceUrl: incomingAvatarSourceUrlProvided
707
+ ? normalizeAiProfileAvatarUrl(profile.avatarSourceUrl)
708
+ : normalizeAiProfileAvatarUrl(existingProfile?.avatarSourceUrl),
709
+ description: profile.description ?? null,
710
+ role: profile.role ?? null,
711
+ provider: profile.provider ?? null,
712
+ model: profile.model ?? null,
713
+ surfaceType: profile.surfaceType ?? null,
714
+ seatScope: profile.seatScope ?? null,
715
+ providerMetadata: profile.providerMetadata ?? null,
716
+ archivedAt: normalizedArchivedAt,
717
+ archivedBy: normalizedArchivedBy,
718
+ archivedReason: normalizedArchivedReason,
719
+ mergedIntoProfileId: normalizedMergedIntoProfileId,
720
+ rosterStateUpdatedAt: normalizedRosterStateUpdatedAt,
721
+ createdAt: normalizedCreatedAt,
722
+ updatedAt: normalizedUpdatedAt,
723
+ lastActiveAt: normalizedLastActiveAt
724
+ }
725
+ : existingProfile;
199
726
  const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
200
- name: profile.name,
201
- description: profile.description,
202
- role: profile.role,
203
- provider: profile.provider,
204
- model: profile.model,
205
- surfaceType: profile.surfaceType,
727
+ name: metadataSourceProfile?.name ?? deps.normalizeAiProfileName(profile.name),
728
+ description: metadataSourceProfile?.description,
729
+ role: metadataSourceProfile?.role,
730
+ provider: metadataSourceProfile?.provider,
731
+ model: metadataSourceProfile?.model,
732
+ surfaceType: metadataSourceProfile?.surfaceType,
206
733
  }, existingProfile);
734
+ const resolvedSeatScope = incomingMetadataWins
735
+ ? resolveSyncedAiProfileSeatScope(deps, profile.seatScope, existingProfile)
736
+ : {
737
+ seatScope: existingProfile?.seatScope ?? null,
738
+ seatScopeProvided: existingProfile?.seatScope !== undefined,
739
+ };
740
+ const mergedProfileToken = incomingMetadataWins
741
+ ? String(profile.profileToken || '').trim()
742
+ : String(existingRow?.profile_token || '').trim();
743
+ const mergedName = metadataSourceProfile?.name ?? deps.normalizeAiProfileName(profile.name);
744
+ const mergedUsername = metadataSourceProfile?.username ?? deps.normalizeAiProfileUsername(profile.username, profile.name);
745
+ const mergedIcon = metadataSourceProfile?.icon ?? deps.normalizeAiProfileIcon(profile.icon);
746
+ const mergedColor = metadataSourceProfile?.color ?? deps.normalizeAiProfileColor(profile.color);
747
+ const existingAvatarUrl = normalizeAiProfileAvatarUrl(existingProfile?.avatarUrl);
748
+ const existingAvatarSourceUrl = normalizeAiProfileAvatarUrl(existingProfile?.avatarSourceUrl);
749
+ const incomingAvatarUrl = incomingAvatarUrlProvided ? normalizeAiProfileAvatarUrl(profile.avatarUrl) : null;
750
+ const incomingAvatarSourceUrl = incomingAvatarSourceUrlProvided ? normalizeAiProfileAvatarUrl(profile.avatarSourceUrl) : null;
751
+ const incomingHasAvatarState = hasAiProfileAvatarState(incomingAvatarUrl, incomingAvatarSourceUrl);
752
+ const existingHasAvatarState = hasAiProfileAvatarState(existingAvatarUrl, existingAvatarSourceUrl);
753
+ let mergedAvatarUrl = existingAvatarUrl;
754
+ let mergedAvatarSourceUrl = existingAvatarSourceUrl;
755
+ let mergedAvatarRevision = existingAvatarRevision;
756
+ let mergedAvatarUpdatedAt = existingAvatarUpdatedAt;
757
+ if (!existingRow) {
758
+ mergedAvatarUrl = incomingAvatarUrlProvided ? incomingAvatarUrl : null;
759
+ mergedAvatarSourceUrl = incomingAvatarSourceUrlProvided ? incomingAvatarSourceUrl : null;
760
+ mergedAvatarRevision = incomingAvatarRevisionProvided ? incomingAvatarRevision : (incomingHasAvatarState ? 1 : 0);
761
+ mergedAvatarUpdatedAt = incomingAvatarUpdatedAt ?? (incomingHasAvatarState ? normalizedUpdatedAt : null);
762
+ }
763
+ else if (incomingAvatarRevisionProvided) {
764
+ if (incomingAvatarRevision > existingAvatarRevision) {
765
+ const incomingClearsAvatar = !incomingHasAvatarState && existingHasAvatarState;
766
+ const clearHasAvatarTimestamp = Boolean(incomingAvatarUpdatedAt);
767
+ if (!incomingClearsAvatar || clearHasAvatarTimestamp) {
768
+ mergedAvatarUrl = incomingAvatarUrlProvided ? incomingAvatarUrl : existingAvatarUrl;
769
+ mergedAvatarSourceUrl = incomingAvatarSourceUrlProvided ? incomingAvatarSourceUrl : existingAvatarSourceUrl;
770
+ mergedAvatarRevision = incomingAvatarRevision;
771
+ mergedAvatarUpdatedAt = incomingAvatarUpdatedAt ?? normalizedUpdatedAt;
772
+ }
773
+ }
774
+ else if (incomingAvatarRevision === existingAvatarRevision) {
775
+ if (!existingHasAvatarState && incomingHasAvatarState) {
776
+ mergedAvatarUrl = incomingAvatarUrl;
777
+ mergedAvatarSourceUrl = incomingAvatarSourceUrl;
778
+ mergedAvatarUpdatedAt = incomingAvatarUpdatedAt ?? existingAvatarUpdatedAt ?? normalizedUpdatedAt;
779
+ }
780
+ }
781
+ }
782
+ else if (!existingHasAvatarState && incomingHasAvatarState) {
783
+ mergedAvatarUrl = incomingAvatarUrl;
784
+ mergedAvatarSourceUrl = incomingAvatarSourceUrl;
785
+ mergedAvatarRevision = Math.max(existingAvatarRevision, 1);
786
+ mergedAvatarUpdatedAt = existingAvatarUpdatedAt ?? normalizedUpdatedAt;
787
+ }
788
+ const mergedProviderMetadata = incomingMetadataWins
789
+ ? (profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
790
+ ? JSON.stringify(profile.providerMetadata)
791
+ : null)
792
+ : (typeof existingRow?.provider_metadata_json === 'string' ? existingRow.provider_metadata_json : null);
793
+ const mergedArchivedAt = incomingLifecycleWins ? normalizedArchivedAt : existingArchivedAt;
794
+ const mergedArchivedBy = incomingLifecycleWins ? normalizedArchivedBy : existingArchivedBy;
795
+ const mergedArchivedReason = incomingLifecycleWins ? normalizedArchivedReason : existingArchivedReason;
796
+ const mergedMergedIntoProfileId = incomingLifecycleWins ? normalizedMergedIntoProfileId : existingMergedIntoProfileId;
797
+ const mergedRosterStateUpdatedAt = incomingLifecycleWins
798
+ ? normalizedRosterStateUpdatedAt
799
+ : existingRosterStateUpdatedAt;
800
+ const mergedCreatedAt = existingCreatedAt || normalizedCreatedAt;
801
+ const mergedUpdatedAt = incomingMetadataWins ? normalizedUpdatedAt : (existingUpdatedAt || normalizedUpdatedAt);
802
+ const mergedLastActiveAt = compareIsoStrings(normalizedLastActiveAt, existingLastActiveAt) > 0
803
+ ? normalizedLastActiveAt
804
+ : existingLastActiveAt;
207
805
  deps.db.prepare(`
208
806
  INSERT INTO ai_profiles (
209
807
  id, tenant_id, workspace_id, profile_token, name, username, icon, color,
210
- description, role, provider, model, surface_type, provider_metadata_json, created_at, updated_at, last_active_at
211
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
808
+ avatar_url, avatar_source_url, avatar_revision, avatar_updated_at,
809
+ description, role, provider, model, surface_type, seat_scope, provider_metadata_json,
810
+ archived_at, archived_by, archived_reason, merged_into_profile_id,
811
+ roster_state_updated_at, created_at, updated_at, last_active_at
812
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
212
813
  ON CONFLICT(id) DO UPDATE SET
213
814
  workspace_id = excluded.workspace_id,
214
815
  profile_token = excluded.profile_token,
@@ -216,18 +817,26 @@ export function upsertAiProfileForSyncService(deps, profile) {
216
817
  username = excluded.username,
217
818
  icon = excluded.icon,
218
819
  color = excluded.color,
820
+ avatar_url = excluded.avatar_url,
821
+ avatar_source_url = excluded.avatar_source_url,
822
+ avatar_revision = excluded.avatar_revision,
823
+ avatar_updated_at = excluded.avatar_updated_at,
219
824
  description = excluded.description,
220
825
  role = excluded.role,
221
826
  provider = excluded.provider,
222
827
  model = excluded.model,
223
828
  surface_type = excluded.surface_type,
829
+ seat_scope = excluded.seat_scope,
224
830
  provider_metadata_json = excluded.provider_metadata_json,
831
+ archived_at = excluded.archived_at,
832
+ archived_by = excluded.archived_by,
833
+ archived_reason = excluded.archived_reason,
834
+ merged_into_profile_id = excluded.merged_into_profile_id,
835
+ roster_state_updated_at = excluded.roster_state_updated_at,
225
836
  created_at = excluded.created_at,
226
837
  updated_at = excluded.updated_at,
227
838
  last_active_at = excluded.last_active_at
228
- `).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), resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceType, profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
229
- ? JSON.stringify(profile.providerMetadata)
230
- : null, normalizedCreatedAt, normalizedUpdatedAt, normalizedLastActiveAt);
839
+ `).run(profileId, deps.tenantId, normalizedWorkspaceId, mergedProfileToken, mergedName, mergedUsername, mergedIcon, mergedColor, mergedAvatarUrl, mergedAvatarSourceUrl, mergedAvatarRevision, mergedAvatarUpdatedAt, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceType, resolvedSeatScope.seatScope, mergedProviderMetadata, mergedArchivedAt, mergedArchivedBy, mergedArchivedReason, mergedMergedIntoProfileId, mergedRosterStateUpdatedAt, mergedCreatedAt, mergedUpdatedAt, mergedLastActiveAt);
231
840
  const refreshed = deps.db.prepare(`
232
841
  SELECT *
233
842
  FROM ai_profiles
@@ -248,15 +857,29 @@ export function mergeAiProfilesService(deps, input) {
248
857
  if (keepId === mergeId)
249
858
  throw deps.createRuleError('keepId and mergeId must be different profiles', 400, 'MERGE_PROFILES_SAME_ID');
250
859
  const keepProfile = deps.db.prepare(`
251
- SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? LIMIT 1
860
+ SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL LIMIT 1
252
861
  `).get(deps.tenantId, normalizedWorkspaceId, keepId);
253
862
  if (!keepProfile)
254
863
  throw deps.createRuleError(`Profile ${keepId} not found`, 404, 'MERGE_PROFILES_KEEP_NOT_FOUND');
255
864
  const mergeProfile = deps.db.prepare(`
256
- SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? LIMIT 1
865
+ SELECT * FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL LIMIT 1
257
866
  `).get(deps.tenantId, normalizedWorkspaceId, mergeId);
258
867
  if (!mergeProfile)
259
868
  throw deps.createRuleError(`Profile ${mergeId} not found`, 404, 'MERGE_PROFILES_MERGE_NOT_FOUND');
869
+ const keepRecord = deps.aiProfileRowToRecord(keepProfile);
870
+ const mergeRecord = deps.aiProfileRowToRecord(mergeProfile);
871
+ const keepSeatScope = keepRecord.seatScope ?? null;
872
+ const mergeSeatScope = mergeRecord.seatScope ?? null;
873
+ if (keepSeatScope && mergeSeatScope && keepSeatScope !== mergeSeatScope) {
874
+ throw deps.createRuleError('Local and cloud AI profiles are separate registrations and cannot be merged. Retire one from the roster instead.', 409, 'AI_PROFILE_SEAT_SCOPE_MISMATCH', {
875
+ workspaceId: normalizedWorkspaceId,
876
+ keepId,
877
+ mergeId,
878
+ keepSeatScope,
879
+ mergeSeatScope,
880
+ });
881
+ }
882
+ const mergedSeatScope = resolveAiProfileSeatScope(deps, mergeRecord.seatScope, keepRecord);
260
883
  const mergedAt = new Date().toISOString();
261
884
  const doMerge = deps.db.transaction(() => {
262
885
  deps.db.prepare(`
@@ -267,13 +890,28 @@ export function mergeAiProfilesService(deps, input) {
267
890
  UPDATE tasks SET created_by = ?, updated_at = ?
268
891
  WHERE tenant_id = ? AND workspace_id = ? AND created_by = ?
269
892
  `).run(keepId, mergedAt, deps.tenantId, normalizedWorkspaceId, mergeId);
893
+ deps.db.prepare(`
894
+ UPDATE ai_profiles
895
+ SET seat_scope = ?,
896
+ avatar_url = COALESCE(avatar_url, ?),
897
+ avatar_source_url = COALESCE(avatar_source_url, ?),
898
+ updated_at = ?
899
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
900
+ `).run(mergedSeatScope.seatScope, normalizeAiProfileAvatarUrl(mergeRecord.avatarUrl), normalizeAiProfileAvatarUrl(mergeRecord.avatarSourceUrl), mergedAt, deps.tenantId, normalizedWorkspaceId, keepId);
270
901
  deps.db.prepare(`
271
902
  UPDATE comments SET author = ?
272
903
  WHERE tenant_id = ? AND workspace_id = ? AND author = ?
273
904
  `).run(keepId, deps.tenantId, normalizedWorkspaceId, mergeId);
274
905
  deps.db.prepare(`
275
- DELETE FROM ai_profiles WHERE tenant_id = ? AND workspace_id = ? AND id = ?
276
- `).run(deps.tenantId, normalizedWorkspaceId, mergeId);
906
+ UPDATE ai_profiles
907
+ SET archived_at = ?,
908
+ archived_by = ?,
909
+ archived_reason = ?,
910
+ merged_into_profile_id = ?,
911
+ roster_state_updated_at = ?,
912
+ updated_at = ?
913
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
914
+ `).run(mergedAt, keepId, 'merged_duplicate', keepId, mergedAt, mergedAt, deps.tenantId, normalizedWorkspaceId, mergeId);
277
915
  });
278
916
  doMerge();
279
917
  const refreshed = deps.db.prepare(`
@@ -281,36 +919,369 @@ export function mergeAiProfilesService(deps, input) {
281
919
  `).get(deps.tenantId, keepId);
282
920
  return hydrateAiProfileRecord(deps, refreshed);
283
921
  }
284
- export function resolveAiProfileService(deps, input) {
922
+ export function updateAiProfileAvatarService(deps, input) {
923
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
924
+ const profileId = String(input.profileId || '').trim();
925
+ if (!profileId)
926
+ throw deps.createRuleError('profileId is required', 400, 'AI_PROFILE_ID_REQUIRED');
927
+ const existing = deps.db.prepare(`
928
+ SELECT *
929
+ FROM ai_profiles
930
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL
931
+ LIMIT 1
932
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
933
+ if (!existing) {
934
+ throw deps.createRuleError(`Profile ${profileId} not found`, 404, 'AI_PROFILE_NOT_FOUND');
935
+ }
936
+ const avatarUrl = normalizeAiProfileAvatarUrl(input.avatarUrl);
937
+ if (typeof input.avatarUrl === 'string' && input.avatarUrl.trim().length > 0 && !avatarUrl) {
938
+ throw deps.createRuleError('avatarUrl must be a Taskforce context URL', 400, 'AI_PROFILE_AVATAR_URL_INVALID');
939
+ }
940
+ const avatarSourceUrl = input.avatarSourceUrl === undefined
941
+ ? normalizeAiProfileAvatarUrl(existing.avatar_source_url)
942
+ : normalizeAiProfileAvatarUrl(input.avatarSourceUrl);
943
+ if (typeof input.avatarSourceUrl === 'string' && input.avatarSourceUrl.trim().length > 0 && !avatarSourceUrl) {
944
+ throw deps.createRuleError('avatarSourceUrl must be a Taskforce context URL', 400, 'AI_PROFILE_AVATAR_SOURCE_URL_INVALID');
945
+ }
946
+ const currentRevision = normalizeAiProfileAvatarRevision(existing.avatar_revision);
947
+ const nextRevision = currentRevision + 1;
948
+ const updatedAt = new Date().toISOString();
949
+ deps.db.prepare(`
950
+ UPDATE ai_profiles
951
+ SET avatar_url = ?,
952
+ avatar_source_url = ?,
953
+ avatar_revision = ?,
954
+ avatar_updated_at = ?,
955
+ updated_at = ?
956
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
957
+ `).run(avatarUrl, avatarSourceUrl, nextRevision, updatedAt, updatedAt, deps.tenantId, normalizedWorkspaceId, profileId);
958
+ if (avatarUrl) {
959
+ deps.db.prepare(`
960
+ UPDATE ai_profiles
961
+ SET avatar_url = ?,
962
+ avatar_source_url = ?,
963
+ avatar_revision = CASE
964
+ WHEN COALESCE(avatar_revision, 0) < ? THEN ?
965
+ ELSE COALESCE(avatar_revision, 0)
966
+ END,
967
+ avatar_updated_at = ?,
968
+ updated_at = ?
969
+ WHERE tenant_id = ?
970
+ AND workspace_id = ?
971
+ AND id <> ?
972
+ AND archived_at IS NULL
973
+ AND (
974
+ LOWER(name) = LOWER(?)
975
+ OR LOWER(username) = LOWER(?)
976
+ )
977
+ AND (avatar_url IS NULL OR TRIM(avatar_url) = '')
978
+ AND (avatar_source_url IS NULL OR TRIM(avatar_source_url) = '')
979
+ `).run(avatarUrl, avatarSourceUrl, nextRevision, nextRevision, updatedAt, updatedAt, deps.tenantId, normalizedWorkspaceId, profileId, String(existing.name || '').trim(), deps.normalizeAiProfileUsername(existing.username, String(existing.name || '').trim()));
980
+ }
981
+ const refreshed = deps.db.prepare(`
982
+ SELECT *
983
+ FROM ai_profiles
984
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
985
+ LIMIT 1
986
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
987
+ if (!refreshed) {
988
+ throw deps.createRuleError('Failed to update AI profile avatar', 500, 'AI_PROFILE_AVATAR_UPDATE_FAILED');
989
+ }
990
+ return hydrateAiProfileRecord(deps, refreshed);
991
+ }
992
+ export function updateAiProfileSurfaceTypeService(deps, input) {
993
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
994
+ const profileId = String(input.profileId || '').trim();
995
+ if (!profileId)
996
+ throw deps.createRuleError('profileId is required', 400, 'AI_PROFILE_ID_REQUIRED');
997
+ const surfaceType = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
998
+ if (surfaceType === undefined) {
999
+ throw deps.createRuleError('surfaceType is required', 400, 'AI_PROFILE_SURFACE_TYPE_REQUIRED');
1000
+ }
1001
+ const existing = deps.db.prepare(`
1002
+ SELECT *
1003
+ FROM ai_profiles
1004
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL
1005
+ LIMIT 1
1006
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
1007
+ if (!existing) {
1008
+ throw deps.createRuleError(`Profile ${profileId} not found`, 404, 'AI_PROFILE_NOT_FOUND');
1009
+ }
1010
+ const providerMetadataJson = surfaceType === null
1011
+ ? clearManualSurfaceTypeOverride(existing)
1012
+ : markManualSurfaceTypeOverride(existing);
1013
+ const updatedAt = new Date().toISOString();
1014
+ deps.db.prepare(`
1015
+ UPDATE ai_profiles
1016
+ SET surface_type = ?,
1017
+ provider_metadata_json = ?,
1018
+ updated_at = ?
1019
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1020
+ `).run(surfaceType, providerMetadataJson, updatedAt, deps.tenantId, normalizedWorkspaceId, profileId);
1021
+ const refreshed = deps.db.prepare(`
1022
+ SELECT *
1023
+ FROM ai_profiles
1024
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1025
+ LIMIT 1
1026
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
1027
+ if (!refreshed) {
1028
+ throw deps.createRuleError('Failed to update AI profile category', 500, 'AI_PROFILE_SURFACE_TYPE_UPDATE_FAILED');
1029
+ }
1030
+ return hydrateAiProfileRecord(deps, refreshed);
1031
+ }
1032
+ export function archiveAiProfileService(deps, input) {
1033
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
1034
+ const profileId = String(input.profileId || '').trim();
1035
+ if (!profileId)
1036
+ throw deps.createRuleError('profileId is required', 400, 'ARCHIVE_AI_PROFILE_INVALID_INPUT');
1037
+ const existingRow = deps.db.prepare(`
1038
+ SELECT *
1039
+ FROM ai_profiles
1040
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ? AND archived_at IS NULL
1041
+ LIMIT 1
1042
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
1043
+ if (!existingRow) {
1044
+ throw deps.createRuleError(`Profile ${profileId} not found`, 404, 'ARCHIVE_AI_PROFILE_NOT_FOUND');
1045
+ }
1046
+ const openTaskRow = deps.db.prepare(`
1047
+ SELECT COUNT(*) AS c
1048
+ FROM tasks
1049
+ WHERE tenant_id = ?
1050
+ AND workspace_id = ?
1051
+ AND assignee = ?
1052
+ AND COALESCE(is_archived, 0) = 0
1053
+ AND LOWER(COALESCE(status, 'task')) NOT IN ('done', 'cancelled')
1054
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
1055
+ const openTaskCount = Math.max(0, Number(openTaskRow?.c || 0));
1056
+ if (openTaskCount > 0) {
1057
+ 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 });
1058
+ }
1059
+ const archivedAt = new Date().toISOString();
1060
+ deps.db.prepare(`
1061
+ UPDATE ai_profiles
1062
+ SET archived_at = ?,
1063
+ archived_by = ?,
1064
+ archived_reason = ?,
1065
+ merged_into_profile_id = NULL,
1066
+ roster_state_updated_at = ?,
1067
+ updated_at = ?
1068
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1069
+ `).run(archivedAt, normalizeNullableProfileReference(input.archivedBy), normalizeOptionalProfileField(input.reason) ?? 'manual_archive', archivedAt, archivedAt, deps.tenantId, normalizedWorkspaceId, profileId);
1070
+ const archivedRow = deps.db.prepare(`
1071
+ SELECT *
1072
+ FROM ai_profiles
1073
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1074
+ LIMIT 1
1075
+ `).get(deps.tenantId, normalizedWorkspaceId, profileId);
1076
+ if (!archivedRow) {
1077
+ throw deps.createRuleError('Failed to archive AI profile', 500, 'ARCHIVE_AI_PROFILE_FAILED');
1078
+ }
1079
+ return {
1080
+ profile: hydrateAiProfileRecord(deps, archivedRow),
1081
+ openTaskCount,
1082
+ };
1083
+ }
1084
+ export function resolveAiProfileService(deps, input, options) {
285
1085
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
286
1086
  const name = deps.normalizeAiProfileName(input.name);
287
1087
  const profileToken = String(input.profileToken || '').trim();
288
1088
  const now = new Date().toISOString();
1089
+ const trustedSeatScope = normalizeAiProfileSeatScopeInput(deps, options?.trustedSeatScope);
289
1090
  if (profileToken) {
290
- const existingRow = deps.db.prepare(`
1091
+ const existingRows = deps.db.prepare(`
291
1092
  SELECT *
292
1093
  FROM ai_profiles
293
1094
  WHERE tenant_id = ?
294
1095
  AND workspace_id = ?
295
1096
  AND profile_token = ?
296
- LIMIT 1
297
- `).get(deps.tenantId, normalizedWorkspaceId, profileToken);
1097
+ AND archived_at IS NULL
1098
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
1099
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken);
1100
+ const existingRow = findFirstAiProfileRowForTrustedSeatScope(deps, existingRows, trustedSeatScope);
298
1101
  if (!existingRow) {
299
- throw deps.createRuleError('AI profile token is invalid for this workspace', 404, 'AI_PROFILE_NOT_FOUND');
1102
+ const archivedRows = deps.db.prepare(`
1103
+ SELECT *
1104
+ FROM ai_profiles
1105
+ WHERE tenant_id = ?
1106
+ AND workspace_id = ?
1107
+ AND profile_token = ?
1108
+ AND archived_at IS NOT NULL
1109
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
1110
+ `).all(deps.tenantId, normalizedWorkspaceId, profileToken);
1111
+ const archivedRow = findFirstAiProfileRowForTrustedSeatScope(deps, archivedRows, trustedSeatScope);
1112
+ if (archivedRow) {
1113
+ const existingProfile = deps.aiProfileRowToRecord(archivedRow);
1114
+ const nextName = name;
1115
+ const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
1116
+ const nextIcon = input.icon === undefined
1117
+ ? existingProfile.icon
1118
+ : deps.normalizeAiProfileIcon(input.icon);
1119
+ const nextColor = input.color === undefined
1120
+ ? existingProfile.color
1121
+ : deps.normalizeAiProfileColor(input.color);
1122
+ const nextDescription = normalizeOptionalProfileField(input.description);
1123
+ const nextRole = normalizeOptionalProfileField(input.role);
1124
+ const nextProvider = normalizeOptionalProfileField(input.provider);
1125
+ const nextModel = normalizeOptionalProfileField(input.model);
1126
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
1127
+ name: nextName,
1128
+ description: nextDescription,
1129
+ role: nextRole,
1130
+ provider: nextProvider,
1131
+ model: nextModel,
1132
+ surfaceType: input.surfaceType,
1133
+ }, existingProfile);
1134
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
1135
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
1136
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1137
+ }
1138
+ deps.db.prepare(`
1139
+ UPDATE ai_profiles
1140
+ SET name = ?,
1141
+ username = ?,
1142
+ icon = ?,
1143
+ color = ?,
1144
+ description = COALESCE(?, description),
1145
+ role = COALESCE(?, role),
1146
+ provider = COALESCE(?, provider),
1147
+ model = COALESCE(?, model),
1148
+ surface_type = CASE
1149
+ WHEN ? = 1 THEN ?
1150
+ ELSE surface_type
1151
+ END,
1152
+ seat_scope = CASE
1153
+ WHEN ? = 1 THEN ?
1154
+ ELSE seat_scope
1155
+ END,
1156
+ archived_at = NULL,
1157
+ archived_by = NULL,
1158
+ archived_reason = NULL,
1159
+ merged_into_profile_id = NULL,
1160
+ roster_state_updated_at = ?,
1161
+ last_active_at = ?,
1162
+ updated_at = ?
1163
+ WHERE tenant_id = ? AND id = ?
1164
+ `).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());
1165
+ const refreshed = deps.db.prepare(`
1166
+ SELECT *
1167
+ FROM ai_profiles
1168
+ WHERE tenant_id = ? AND id = ?
1169
+ LIMIT 1
1170
+ `).get(deps.tenantId, String(archivedRow.id || '').trim());
1171
+ if (!refreshed) {
1172
+ throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
1173
+ }
1174
+ return {
1175
+ created: false,
1176
+ profile: hydrateAiProfileRecord(deps, refreshed)
1177
+ };
1178
+ }
1179
+ // Treat token misses as recoverable when the caller also supplied profile
1180
+ // identity metadata; the name/seat-scope resolver below can bind stale
1181
+ // reconnects without minting duplicates.
1182
+ }
1183
+ else {
1184
+ const existingProfile = deps.aiProfileRowToRecord(existingRow);
1185
+ const nextName = name;
1186
+ const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
1187
+ const nextIcon = input.icon === undefined
1188
+ ? existingProfile.icon
1189
+ : deps.normalizeAiProfileIcon(input.icon);
1190
+ const nextColor = input.color === undefined
1191
+ ? existingProfile.color
1192
+ : deps.normalizeAiProfileColor(input.color);
1193
+ const nextDescription = normalizeOptionalProfileField(input.description);
1194
+ const nextRole = normalizeOptionalProfileField(input.role);
1195
+ const nextProvider = normalizeOptionalProfileField(input.provider);
1196
+ const nextModel = normalizeOptionalProfileField(input.model);
1197
+ const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
1198
+ name: nextName,
1199
+ description: nextDescription,
1200
+ role: nextRole,
1201
+ provider: nextProvider,
1202
+ model: nextModel,
1203
+ surfaceType: input.surfaceType,
1204
+ }, existingProfile);
1205
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
1206
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
1207
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1208
+ }
1209
+ deps.db.prepare(`
1210
+ UPDATE ai_profiles
1211
+ SET name = ?,
1212
+ username = ?,
1213
+ icon = ?,
1214
+ color = ?,
1215
+ description = COALESCE(?, description),
1216
+ role = COALESCE(?, role),
1217
+ provider = COALESCE(?, provider),
1218
+ model = COALESCE(?, model),
1219
+ surface_type = CASE
1220
+ WHEN ? = 1 THEN ?
1221
+ ELSE surface_type
1222
+ END,
1223
+ seat_scope = CASE
1224
+ WHEN ? = 1 THEN ?
1225
+ ELSE seat_scope
1226
+ END,
1227
+ last_active_at = ?,
1228
+ updated_at = ?
1229
+ WHERE tenant_id = ? AND id = ?
1230
+ `).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());
1231
+ const refreshed = deps.db.prepare(`
1232
+ SELECT *
1233
+ FROM ai_profiles
1234
+ WHERE tenant_id = ? AND id = ?
1235
+ LIMIT 1
1236
+ `).get(deps.tenantId, String(existingRow.id || '').trim());
1237
+ if (!refreshed) {
1238
+ throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
1239
+ }
1240
+ return {
1241
+ created: false,
1242
+ profile: hydrateAiProfileRecord(deps, refreshed)
1243
+ };
300
1244
  }
301
- const existingProfile = deps.aiProfileRowToRecord(existingRow);
1245
+ }
1246
+ const identityDescription = normalizeOptionalProfileField(input.description);
1247
+ const identityRole = normalizeOptionalProfileField(input.role);
1248
+ const inferredIdentity = inferAiProfileProviderModelDefaults(name);
1249
+ const identityProvider = normalizeOptionalProfileField(input.provider)
1250
+ ?? normalizeOptionalProfileField(inferredIdentity.provider);
1251
+ const identityModel = normalizeOptionalProfileField(input.model)
1252
+ ?? normalizeOptionalProfileField(inferredIdentity.model);
1253
+ const identityMetadata = resolveAiProfileMetadataFields(deps, {
1254
+ name,
1255
+ description: identityDescription,
1256
+ role: identityRole,
1257
+ provider: identityProvider,
1258
+ model: identityModel,
1259
+ surfaceType: input.surfaceType,
1260
+ }, null);
1261
+ const exactMatches = deps.db.prepare(`
1262
+ SELECT *
1263
+ FROM ai_profiles
1264
+ WHERE tenant_id = ?
1265
+ AND workspace_id = ?
1266
+ AND archived_at IS NULL
1267
+ AND (
1268
+ LOWER(name) = LOWER(?)
1269
+ OR LOWER(username) = LOWER(?)
1270
+ )
1271
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
1272
+ `).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
1273
+ const exactMatch = selectAiProfileIdentityMatch(deps, exactMatches, identityMetadata, trustedSeatScope);
1274
+ if (exactMatch.row) {
1275
+ const canonicalId = String(exactMatch.row.id || '').trim();
302
1276
  const nextName = name;
303
1277
  const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
304
- const nextIcon = input.icon === undefined
305
- ? existingProfile.icon
306
- : deps.normalizeAiProfileIcon(input.icon);
307
- const nextColor = input.color === undefined
308
- ? existingProfile.color
309
- : deps.normalizeAiProfileColor(input.color);
1278
+ const nextIcon = deps.normalizeAiProfileIcon(input.icon);
1279
+ const nextColor = deps.normalizeAiProfileColor(input.color);
310
1280
  const nextDescription = normalizeOptionalProfileField(input.description);
311
1281
  const nextRole = normalizeOptionalProfileField(input.role);
312
1282
  const nextProvider = normalizeOptionalProfileField(input.provider);
313
1283
  const nextModel = normalizeOptionalProfileField(input.model);
1284
+ const existingProfile = deps.aiProfileRowToRecord(exactMatch.row);
314
1285
  const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
315
1286
  name: nextName,
316
1287
  description: nextDescription,
@@ -319,6 +1290,10 @@ export function resolveAiProfileService(deps, input) {
319
1290
  model: nextModel,
320
1291
  surfaceType: input.surfaceType,
321
1292
  }, existingProfile);
1293
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
1294
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
1295
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1296
+ }
322
1297
  deps.db.prepare(`
323
1298
  UPDATE ai_profiles
324
1299
  SET name = ?,
@@ -329,41 +1304,53 @@ export function resolveAiProfileService(deps, input) {
329
1304
  role = COALESCE(?, role),
330
1305
  provider = COALESCE(?, provider),
331
1306
  model = COALESCE(?, model),
1307
+ last_active_at = ?,
1308
+ updated_at = ?,
332
1309
  surface_type = CASE
333
1310
  WHEN ? = 1 THEN ?
334
1311
  ELSE surface_type
335
1312
  END,
336
- last_active_at = ?,
337
- updated_at = ?
1313
+ seat_scope = CASE
1314
+ WHEN ? = 1 THEN ?
1315
+ ELSE seat_scope
1316
+ END
338
1317
  WHERE tenant_id = ? AND id = ?
339
- `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, now, now, deps.tenantId, String(existingRow.id || '').trim());
340
- const refreshed = deps.db.prepare(`
1318
+ `).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);
1319
+ const reused = deps.db.prepare(`
341
1320
  SELECT *
342
1321
  FROM ai_profiles
343
1322
  WHERE tenant_id = ? AND id = ?
344
1323
  LIMIT 1
345
- `).get(deps.tenantId, String(existingRow.id || '').trim());
346
- if (!refreshed) {
1324
+ `).get(deps.tenantId, canonicalId);
1325
+ if (!reused) {
347
1326
  throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
348
1327
  }
349
1328
  return {
350
1329
  created: false,
351
- profile: hydrateAiProfileRecord(deps, refreshed)
1330
+ profile: hydrateAiProfileRecord(deps, reused),
1331
+ ...(exactMatch.matchCount > 1 ? {
1332
+ duplicateWarning: {
1333
+ existingProfileId: canonicalId,
1334
+ message: `Multiple profiles matched "${name}". Reused ${canonicalId}; merge the remaining duplicates.`
1335
+ }
1336
+ } : {})
352
1337
  };
353
1338
  }
354
- const exactMatches = deps.db.prepare(`
1339
+ const archivedMatches = deps.db.prepare(`
355
1340
  SELECT *
356
1341
  FROM ai_profiles
357
1342
  WHERE tenant_id = ?
358
1343
  AND workspace_id = ?
1344
+ AND archived_at IS NOT NULL
359
1345
  AND (
360
1346
  LOWER(name) = LOWER(?)
361
1347
  OR LOWER(username) = LOWER(?)
362
1348
  )
363
1349
  ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
364
1350
  `).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
365
- if (exactMatches.length > 0) {
366
- const canonicalId = String(exactMatches[0].id || '').trim();
1351
+ const archivedMatch = selectAiProfileIdentityMatch(deps, archivedMatches, identityMetadata, trustedSeatScope);
1352
+ if (archivedMatch.row) {
1353
+ const canonicalId = String(archivedMatch.row.id || '').trim();
367
1354
  const nextName = name;
368
1355
  const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
369
1356
  const nextIcon = deps.normalizeAiProfileIcon(input.icon);
@@ -372,7 +1359,7 @@ export function resolveAiProfileService(deps, input) {
372
1359
  const nextRole = normalizeOptionalProfileField(input.role);
373
1360
  const nextProvider = normalizeOptionalProfileField(input.provider);
374
1361
  const nextModel = normalizeOptionalProfileField(input.model);
375
- const existingProfile = deps.aiProfileRowToRecord(exactMatches[0]);
1362
+ const existingProfile = deps.aiProfileRowToRecord(archivedMatch.row);
376
1363
  const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
377
1364
  name: nextName,
378
1365
  description: nextDescription,
@@ -381,6 +1368,10 @@ export function resolveAiProfileService(deps, input) {
381
1368
  model: nextModel,
382
1369
  surfaceType: input.surfaceType,
383
1370
  }, existingProfile);
1371
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, existingProfile);
1372
+ if (requiresCloudSeatReservation(existingProfile, resolvedSeatScope.seatScope)) {
1373
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1374
+ }
384
1375
  deps.db.prepare(`
385
1376
  UPDATE ai_profiles
386
1377
  SET name = ?,
@@ -393,41 +1384,35 @@ export function resolveAiProfileService(deps, input) {
393
1384
  model = COALESCE(?, model),
394
1385
  last_active_at = ?,
395
1386
  updated_at = ?,
1387
+ archived_at = NULL,
1388
+ archived_by = NULL,
1389
+ archived_reason = NULL,
1390
+ merged_into_profile_id = NULL,
1391
+ roster_state_updated_at = ?,
396
1392
  surface_type = CASE
397
1393
  WHEN ? = 1 THEN ?
398
1394
  ELSE surface_type
1395
+ END,
1396
+ seat_scope = CASE
1397
+ WHEN ? = 1 THEN ?
1398
+ ELSE seat_scope
399
1399
  END
400
1400
  WHERE tenant_id = ? AND id = ?
401
- `).run(nextName, nextUsername, nextIcon, nextColor, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, now, now, resolvedMetadata.surfaceTypeProvided ? 1 : 0, resolvedMetadata.surfaceType, deps.tenantId, canonicalId);
402
- const reused = deps.db.prepare(`
1401
+ `).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);
1402
+ const reactivated = deps.db.prepare(`
403
1403
  SELECT *
404
1404
  FROM ai_profiles
405
1405
  WHERE tenant_id = ? AND id = ?
406
1406
  LIMIT 1
407
1407
  `).get(deps.tenantId, canonicalId);
408
- if (!reused) {
1408
+ if (!reactivated) {
409
1409
  throw deps.createRuleError('Failed to refresh AI profile', 500, 'AI_PROFILE_REFRESH_FAILED');
410
1410
  }
411
1411
  return {
412
1412
  created: false,
413
- profile: hydrateAiProfileRecord(deps, reused),
414
- ...(exactMatches.length > 1 ? {
415
- duplicateWarning: {
416
- existingProfileId: canonicalId,
417
- message: `Multiple profiles matched "${name}". Reused ${canonicalId}; merge the remaining duplicates.`
418
- }
419
- } : {})
1413
+ profile: hydrateAiProfileRecord(deps, reactivated)
420
1414
  };
421
1415
  }
422
- const aiProfileUsage = deps.countAiProfileUsage(normalizedWorkspaceId);
423
- if (aiProfileUsage.limit !== null && aiProfileUsage.used >= aiProfileUsage.limit) {
424
- throw deps.createRuleError('No AI seats are available for this workspace. Remove an existing AI profile or upgrade your plan.', 403, 'AI_PROFILE_LIMIT_REACHED', {
425
- workspaceId: normalizedWorkspaceId,
426
- limit: aiProfileUsage.limit,
427
- used: aiProfileUsage.used,
428
- remaining: aiProfileUsage.remaining
429
- });
430
- }
431
1416
  const id = deps.generateEntityId('ai-profile');
432
1417
  const issuedProfileToken = deps.issueProfileToken();
433
1418
  const username = deps.normalizeAiProfileUsername(undefined, name);
@@ -445,15 +1430,21 @@ export function resolveAiProfileService(deps, input) {
445
1430
  model,
446
1431
  surfaceType: input.surfaceType,
447
1432
  }, null);
1433
+ const resolvedSeatScope = resolveTrustedAiProfileSeatScope(deps, trustedSeatScope, null);
1434
+ if (resolvedSeatScope.seatScope === 'cloud_metered') {
1435
+ assertAiProfileSeatAvailable(deps, normalizedWorkspaceId);
1436
+ }
448
1437
  const providerMetadata = input.providerMetadata && typeof input.providerMetadata === 'object'
449
1438
  ? input.providerMetadata
450
1439
  : null;
451
1440
  deps.db.prepare(`
452
1441
  INSERT INTO ai_profiles (
453
1442
  id, tenant_id, workspace_id, profile_token, name, username, icon, color,
454
- description, role, provider, model, surface_type, provider_metadata_json, created_at, updated_at, last_active_at
455
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
456
- `).run(id, deps.tenantId, normalizedWorkspaceId, issuedProfileToken, name, username, icon, color, resolvedMetadata.description, resolvedMetadata.role, resolvedMetadata.provider, resolvedMetadata.model, resolvedMetadata.surfaceType, providerMetadata ? JSON.stringify(providerMetadata) : null, now, now, now);
1443
+ avatar_url, avatar_source_url, description, role, provider, model, surface_type, seat_scope, provider_metadata_json,
1444
+ archived_at, archived_by, archived_reason, merged_into_profile_id,
1445
+ roster_state_updated_at, created_at, updated_at, last_active_at
1446
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1447
+ `).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);
457
1448
  const created = deps.db.prepare(`
458
1449
  SELECT *
459
1450
  FROM ai_profiles