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