@taskforcehq/taskforce 0.3.306 → 0.3.308
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 +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- 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 +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- 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/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -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 +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/PlanningEntities.test.js +366 -4
- 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/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- 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/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- 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 +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- 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.js +58 -15
- 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 +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- 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/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- 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 +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -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/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- 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/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -22,6 +22,17 @@ function normalizeOptionalProfileField(value) {
|
|
|
22
22
|
function normalizeAiProfileAvatarUrl(value) {
|
|
23
23
|
return normalizeSyncSameOriginAssetUrl(value);
|
|
24
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
|
+
}
|
|
25
36
|
function compareIsoStrings(left, right) {
|
|
26
37
|
const normalizedLeft = normalizeNullableIsoString(left);
|
|
27
38
|
const normalizedRight = normalizeNullableIsoString(right);
|
|
@@ -53,16 +64,73 @@ function normalizeAiProfileSurfaceTypeInput(deps, value) {
|
|
|
53
64
|
if (value === null)
|
|
54
65
|
return null;
|
|
55
66
|
if (typeof value !== 'string') {
|
|
56
|
-
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');
|
|
57
68
|
}
|
|
58
69
|
const normalized = value.trim();
|
|
59
70
|
if (!normalized)
|
|
60
71
|
return null;
|
|
72
|
+
if (normalized === 'other')
|
|
73
|
+
return 'agent';
|
|
61
74
|
if (!isAiProfileSurfaceType(normalized)) {
|
|
62
|
-
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');
|
|
63
76
|
}
|
|
64
77
|
return normalized;
|
|
65
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
|
+
}
|
|
66
134
|
function normalizeAiProfileSeatScopeInput(deps, value) {
|
|
67
135
|
if (value === undefined)
|
|
68
136
|
return undefined;
|
|
@@ -145,6 +213,66 @@ function filterAiProfileRowsForTrustedSeatScope(deps, rows, trustedSeatScope) {
|
|
|
145
213
|
return rows;
|
|
146
214
|
return rows.filter((row) => aiProfileMatchesTrustedSeatScope(deps, row, trustedSeatScope));
|
|
147
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
|
+
}
|
|
148
276
|
function requiresCloudSeatReservation(existing, nextSeatScope) {
|
|
149
277
|
if (nextSeatScope !== 'cloud_metered')
|
|
150
278
|
return false;
|
|
@@ -194,6 +322,12 @@ function inferAiProfileMetadataDefaults(name, surfaceType) {
|
|
|
194
322
|
role: 'Coding Agent',
|
|
195
323
|
...inferredProviderModel
|
|
196
324
|
};
|
|
325
|
+
case 'cli':
|
|
326
|
+
return {
|
|
327
|
+
description: `${name} CLI coding agent connected through MCP.`,
|
|
328
|
+
role: 'CLI Coding Agent',
|
|
329
|
+
...inferredProviderModel
|
|
330
|
+
};
|
|
197
331
|
case 'chat_app':
|
|
198
332
|
return {
|
|
199
333
|
description: `${name} conversational AI connected through Taskforce.`,
|
|
@@ -206,12 +340,6 @@ function inferAiProfileMetadataDefaults(name, surfaceType) {
|
|
|
206
340
|
role: 'Coding Assistant',
|
|
207
341
|
...inferredProviderModel
|
|
208
342
|
};
|
|
209
|
-
case 'other':
|
|
210
|
-
return {
|
|
211
|
-
description: `${name} AI profile connected through Taskforce.`,
|
|
212
|
-
role: 'AI Assistant',
|
|
213
|
-
...inferredProviderModel
|
|
214
|
-
};
|
|
215
343
|
default:
|
|
216
344
|
return {};
|
|
217
345
|
}
|
|
@@ -227,7 +355,9 @@ function resolveOptionalProfileField(inputValue, existingValue, fallbackValue) {
|
|
|
227
355
|
}
|
|
228
356
|
function resolveAiProfileMetadataFields(deps, input, existing) {
|
|
229
357
|
const surfaceTypeInput = normalizeAiProfileSurfaceTypeInput(deps, input.surfaceType);
|
|
358
|
+
const manuallyOverridden = hasManualSurfaceTypeOverride(existing);
|
|
230
359
|
const effectiveSurfaceType = surfaceTypeInput === undefined
|
|
360
|
+
|| manuallyOverridden
|
|
231
361
|
? existing?.surfaceType ?? null
|
|
232
362
|
: surfaceTypeInput;
|
|
233
363
|
const inferredDefaults = inferAiProfileMetadataDefaults(input.name, effectiveSurfaceType);
|
|
@@ -237,7 +367,7 @@ function resolveAiProfileMetadataFields(deps, input, existing) {
|
|
|
237
367
|
provider: resolveOptionalProfileField(input.provider, existing?.provider, inferredDefaults.provider) ?? null,
|
|
238
368
|
model: resolveOptionalProfileField(input.model, existing?.model, inferredDefaults.model) ?? null,
|
|
239
369
|
surfaceType: effectiveSurfaceType,
|
|
240
|
-
surfaceTypeProvided: surfaceTypeInput !== undefined,
|
|
370
|
+
surfaceTypeProvided: surfaceTypeInput !== undefined && !manuallyOverridden,
|
|
241
371
|
};
|
|
242
372
|
}
|
|
243
373
|
function applyAiProfileMetadataDefaults(record) {
|
|
@@ -250,7 +380,167 @@ function applyAiProfileMetadataDefaults(record) {
|
|
|
250
380
|
model: resolveOptionalProfileField(undefined, record.model, inferredDefaults.model) ?? null,
|
|
251
381
|
};
|
|
252
382
|
}
|
|
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
|
+
}
|
|
253
541
|
function hydrateAiProfileRecord(deps, row, diagnostics) {
|
|
542
|
+
backfillMissingAiProfileAvatarFromDuplicate(deps, row);
|
|
543
|
+
backfillMissingAiProfileAvatarFromAssetIndex(deps, row);
|
|
254
544
|
const record = deps.aiProfileRowToRecord(row);
|
|
255
545
|
const hydrated = applyAiProfileMetadataDefaults(record);
|
|
256
546
|
const metadataChanged = record.description !== hydrated.description
|
|
@@ -286,6 +576,8 @@ export function listAiProfilesService(deps, workspaceId, options) {
|
|
|
286
576
|
color,
|
|
287
577
|
avatar_url,
|
|
288
578
|
avatar_source_url,
|
|
579
|
+
avatar_revision,
|
|
580
|
+
avatar_updated_at,
|
|
289
581
|
description,
|
|
290
582
|
role,
|
|
291
583
|
provider,
|
|
@@ -358,6 +650,9 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
358
650
|
const existingProfile = existingRow ? deps.aiProfileRowToRecord(existingRow) : null;
|
|
359
651
|
const incomingAvatarUrlProvided = Object.prototype.hasOwnProperty.call(profile, 'avatarUrl');
|
|
360
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);
|
|
361
656
|
const existingCreatedAt = existingRow
|
|
362
657
|
? String(existingRow.created_at || existingRow.updated_at || '').trim()
|
|
363
658
|
: '';
|
|
@@ -386,6 +681,8 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
386
681
|
updatedAt: existingUpdatedAt,
|
|
387
682
|
})
|
|
388
683
|
: null;
|
|
684
|
+
const existingAvatarRevision = normalizeAiProfileAvatarRevision(existingRow?.avatar_revision);
|
|
685
|
+
const existingAvatarUpdatedAt = normalizeNullableIsoString(existingRow?.avatar_updated_at);
|
|
389
686
|
const metadataComparison = compareIsoStrings(normalizedUpdatedAt, existingUpdatedAt);
|
|
390
687
|
const incomingMetadataWins = !existingRow || metadataComparison > 0;
|
|
391
688
|
const lifecycleComparison = compareIsoStrings(normalizedRosterStateUpdatedAt, existingRosterStateUpdatedAt);
|
|
@@ -447,13 +744,47 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
447
744
|
const mergedUsername = metadataSourceProfile?.username ?? deps.normalizeAiProfileUsername(profile.username, profile.name);
|
|
448
745
|
const mergedIcon = metadataSourceProfile?.icon ?? deps.normalizeAiProfileIcon(profile.icon);
|
|
449
746
|
const mergedColor = metadataSourceProfile?.color ?? deps.normalizeAiProfileColor(profile.color);
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
788
|
const mergedProviderMetadata = incomingMetadataWins
|
|
458
789
|
? (profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
|
|
459
790
|
? JSON.stringify(profile.providerMetadata)
|
|
@@ -474,10 +805,11 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
474
805
|
deps.db.prepare(`
|
|
475
806
|
INSERT INTO ai_profiles (
|
|
476
807
|
id, tenant_id, workspace_id, profile_token, name, username, icon, color,
|
|
477
|
-
avatar_url, avatar_source_url,
|
|
808
|
+
avatar_url, avatar_source_url, avatar_revision, avatar_updated_at,
|
|
809
|
+
description, role, provider, model, surface_type, seat_scope, provider_metadata_json,
|
|
478
810
|
archived_at, archived_by, archived_reason, merged_into_profile_id,
|
|
479
811
|
roster_state_updated_at, created_at, updated_at, last_active_at
|
|
480
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
812
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
481
813
|
ON CONFLICT(id) DO UPDATE SET
|
|
482
814
|
workspace_id = excluded.workspace_id,
|
|
483
815
|
profile_token = excluded.profile_token,
|
|
@@ -487,6 +819,8 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
487
819
|
color = excluded.color,
|
|
488
820
|
avatar_url = excluded.avatar_url,
|
|
489
821
|
avatar_source_url = excluded.avatar_source_url,
|
|
822
|
+
avatar_revision = excluded.avatar_revision,
|
|
823
|
+
avatar_updated_at = excluded.avatar_updated_at,
|
|
490
824
|
description = excluded.description,
|
|
491
825
|
role = excluded.role,
|
|
492
826
|
provider = excluded.provider,
|
|
@@ -502,7 +836,7 @@ export function upsertAiProfileForSyncService(deps, profile) {
|
|
|
502
836
|
created_at = excluded.created_at,
|
|
503
837
|
updated_at = excluded.updated_at,
|
|
504
838
|
last_active_at = excluded.last_active_at
|
|
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);
|
|
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);
|
|
506
840
|
const refreshed = deps.db.prepare(`
|
|
507
841
|
SELECT *
|
|
508
842
|
FROM ai_profiles
|
|
@@ -534,6 +868,17 @@ export function mergeAiProfilesService(deps, input) {
|
|
|
534
868
|
throw deps.createRuleError(`Profile ${mergeId} not found`, 404, 'MERGE_PROFILES_MERGE_NOT_FOUND');
|
|
535
869
|
const keepRecord = deps.aiProfileRowToRecord(keepProfile);
|
|
536
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
|
+
}
|
|
537
882
|
const mergedSeatScope = resolveAiProfileSeatScope(deps, mergeRecord.seatScope, keepRecord);
|
|
538
883
|
const mergedAt = new Date().toISOString();
|
|
539
884
|
const doMerge = deps.db.transaction(() => {
|
|
@@ -598,14 +943,41 @@ export function updateAiProfileAvatarService(deps, input) {
|
|
|
598
943
|
if (typeof input.avatarSourceUrl === 'string' && input.avatarSourceUrl.trim().length > 0 && !avatarSourceUrl) {
|
|
599
944
|
throw deps.createRuleError('avatarSourceUrl must be a Taskforce context URL', 400, 'AI_PROFILE_AVATAR_SOURCE_URL_INVALID');
|
|
600
945
|
}
|
|
946
|
+
const currentRevision = normalizeAiProfileAvatarRevision(existing.avatar_revision);
|
|
947
|
+
const nextRevision = currentRevision + 1;
|
|
601
948
|
const updatedAt = new Date().toISOString();
|
|
602
949
|
deps.db.prepare(`
|
|
603
950
|
UPDATE ai_profiles
|
|
604
951
|
SET avatar_url = ?,
|
|
605
952
|
avatar_source_url = ?,
|
|
953
|
+
avatar_revision = ?,
|
|
954
|
+
avatar_updated_at = ?,
|
|
606
955
|
updated_at = ?
|
|
607
956
|
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
608
|
-
`).run(avatarUrl, avatarSourceUrl, updatedAt, deps.tenantId, normalizedWorkspaceId, profileId);
|
|
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
|
+
}
|
|
609
981
|
const refreshed = deps.db.prepare(`
|
|
610
982
|
SELECT *
|
|
611
983
|
FROM ai_profiles
|
|
@@ -617,6 +989,46 @@ export function updateAiProfileAvatarService(deps, input) {
|
|
|
617
989
|
}
|
|
618
990
|
return hydrateAiProfileRecord(deps, refreshed);
|
|
619
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
|
+
}
|
|
620
1032
|
export function archiveAiProfileService(deps, input) {
|
|
621
1033
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
|
|
622
1034
|
const profileId = String(input.profileId || '').trim();
|
|
@@ -697,12 +1109,7 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
697
1109
|
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
698
1110
|
`).all(deps.tenantId, normalizedWorkspaceId, profileToken);
|
|
699
1111
|
const archivedRow = findFirstAiProfileRowForTrustedSeatScope(deps, archivedRows, trustedSeatScope);
|
|
700
|
-
if (
|
|
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 {
|
|
1112
|
+
if (archivedRow) {
|
|
706
1113
|
const existingProfile = deps.aiProfileRowToRecord(archivedRow);
|
|
707
1114
|
const nextName = name;
|
|
708
1115
|
const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
|
|
@@ -769,6 +1176,9 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
769
1176
|
profile: hydrateAiProfileRecord(deps, refreshed)
|
|
770
1177
|
};
|
|
771
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.
|
|
772
1182
|
}
|
|
773
1183
|
else {
|
|
774
1184
|
const existingProfile = deps.aiProfileRowToRecord(existingRow);
|
|
@@ -833,6 +1243,21 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
833
1243
|
};
|
|
834
1244
|
}
|
|
835
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);
|
|
836
1261
|
const exactMatches = deps.db.prepare(`
|
|
837
1262
|
SELECT *
|
|
838
1263
|
FROM ai_profiles
|
|
@@ -845,9 +1270,9 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
845
1270
|
)
|
|
846
1271
|
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
847
1272
|
`).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
|
|
848
|
-
const
|
|
849
|
-
if (
|
|
850
|
-
const canonicalId = String(
|
|
1273
|
+
const exactMatch = selectAiProfileIdentityMatch(deps, exactMatches, identityMetadata, trustedSeatScope);
|
|
1274
|
+
if (exactMatch.row) {
|
|
1275
|
+
const canonicalId = String(exactMatch.row.id || '').trim();
|
|
851
1276
|
const nextName = name;
|
|
852
1277
|
const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
|
|
853
1278
|
const nextIcon = deps.normalizeAiProfileIcon(input.icon);
|
|
@@ -856,7 +1281,7 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
856
1281
|
const nextRole = normalizeOptionalProfileField(input.role);
|
|
857
1282
|
const nextProvider = normalizeOptionalProfileField(input.provider);
|
|
858
1283
|
const nextModel = normalizeOptionalProfileField(input.model);
|
|
859
|
-
const existingProfile = deps.aiProfileRowToRecord(
|
|
1284
|
+
const existingProfile = deps.aiProfileRowToRecord(exactMatch.row);
|
|
860
1285
|
const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
|
|
861
1286
|
name: nextName,
|
|
862
1287
|
description: nextDescription,
|
|
@@ -903,7 +1328,7 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
903
1328
|
return {
|
|
904
1329
|
created: false,
|
|
905
1330
|
profile: hydrateAiProfileRecord(deps, reused),
|
|
906
|
-
...(
|
|
1331
|
+
...(exactMatch.matchCount > 1 ? {
|
|
907
1332
|
duplicateWarning: {
|
|
908
1333
|
existingProfileId: canonicalId,
|
|
909
1334
|
message: `Multiple profiles matched "${name}". Reused ${canonicalId}; merge the remaining duplicates.`
|
|
@@ -923,9 +1348,9 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
923
1348
|
)
|
|
924
1349
|
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
925
1350
|
`).all(deps.tenantId, normalizedWorkspaceId, name, deps.normalizeAiProfileUsername(undefined, name));
|
|
926
|
-
const
|
|
927
|
-
if (
|
|
928
|
-
const canonicalId = String(
|
|
1351
|
+
const archivedMatch = selectAiProfileIdentityMatch(deps, archivedMatches, identityMetadata, trustedSeatScope);
|
|
1352
|
+
if (archivedMatch.row) {
|
|
1353
|
+
const canonicalId = String(archivedMatch.row.id || '').trim();
|
|
929
1354
|
const nextName = name;
|
|
930
1355
|
const nextUsername = deps.normalizeAiProfileUsername(undefined, nextName);
|
|
931
1356
|
const nextIcon = deps.normalizeAiProfileIcon(input.icon);
|
|
@@ -934,7 +1359,7 @@ export function resolveAiProfileService(deps, input, options) {
|
|
|
934
1359
|
const nextRole = normalizeOptionalProfileField(input.role);
|
|
935
1360
|
const nextProvider = normalizeOptionalProfileField(input.provider);
|
|
936
1361
|
const nextModel = normalizeOptionalProfileField(input.model);
|
|
937
|
-
const existingProfile = deps.aiProfileRowToRecord(
|
|
1362
|
+
const existingProfile = deps.aiProfileRowToRecord(archivedMatch.row);
|
|
938
1363
|
const resolvedMetadata = resolveAiProfileMetadataFields(deps, {
|
|
939
1364
|
name: nextName,
|
|
940
1365
|
description: nextDescription,
|