@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.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +603 -107
- package/dist/TaskforceCore.js +216 -43
- package/dist/TaskforceCore.test.js +611 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +561 -0
- package/dist/components/features/AgentsModule.d.ts +14 -1
- package/dist/components/features/AgentsModule.js +366 -18
- package/dist/components/features/AgentsModule.test.js +507 -8
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +7 -1
- package/dist/components/features/DocumentViewer.js +62 -4
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +51 -1
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +24 -29
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +61 -116
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -21
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +78 -14
- package/dist/components/views/PlanComparisonPage.test.js +257 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +529 -592
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
- package/dist/components/views/panels/PlanningDrawer.js +270 -55
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +46 -2
- package/dist/core/AiProfileService.js +1073 -82
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +1249 -44
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +110 -23
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +127 -6
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +418 -4
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +415 -40
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +1543 -266
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +75 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
- package/dist/hooks/sync/orchestratorShared.js +129 -48
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +179 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +16 -3
- package/dist/hooks/sync/transfers.js +210 -66
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +928 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +409 -183
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +145 -52
- package/dist/hooks/useTaskforce.js +592 -253
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +13 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +116 -10
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.js +1099 -185
- package/dist/mcp/runtime.test.js +1491 -140
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +192 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +124 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +111 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +459 -28
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.js +1170 -71
- package/dist/server/routes/authSupport.d.ts +5 -1
- package/dist/server/routes/authSupport.js +13 -2
- package/dist/server/routes/billing.js +372 -35
- package/dist/server/routes/billing.test.js +595 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +182 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -14
- package/dist/server/routes/sync.integration.test.js +658 -39
- package/dist/server/routes/sync.js +74 -608
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +182 -0
- package/dist/server/routes/tasks.js +55 -10
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +147 -11
- package/dist/server/routes.test.js +2704 -183
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +19 -11
- package/dist/sync/collaborationSyncPayload.test.js +14 -1
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +9 -9
- package/dist/sync/contentSyncState.js +248 -107
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +11 -4
- package/dist/sync/syncApplyHandlers.js +115 -42
- package/dist/sync/syncApplyHandlers.test.js +251 -4
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +46 -0
- package/dist/sync/syncResourceAdapters.js +79 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +65 -1
- package/dist/sync/syncService.test.js +96 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +132 -72
- package/dist/sync/workspacePullFeed.test.js +92 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +73 -6
- package/dist/sync/workspaceSyncModel.js +342 -73
- package/dist/sync/workspaceSyncModel.test.js +559 -14
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +50 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +8 -0
- package/dist/utils/assignees.js +10 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +9 -1
- package/dist/utils/taskActivity.js +290 -22
- package/dist/utils/taskActivity.test.js +141 -8
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- 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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
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:
|
|
202
|
-
role:
|
|
203
|
-
provider:
|
|
204
|
-
model:
|
|
205
|
-
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
|
-
|
|
211
|
-
|
|
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,
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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,
|
|
340
|
-
const
|
|
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,
|
|
346
|
-
if (!
|
|
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,
|
|
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
|
|
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
|
-
|
|
366
|
-
|
|
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(
|
|
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
|
|
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 (!
|
|
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,
|
|
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,
|
|
455
|
-
|
|
456
|
-
|
|
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
|