s3db.js 13.1.0 → 13.2.2

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.
@@ -72,10 +72,7 @@ export class MLPlugin extends Plugin {
72
72
  };
73
73
 
74
74
  // Validate TensorFlow.js dependency
75
- requirePluginDependency('@tensorflow/tfjs-node', 'MLPlugin', {
76
- installCommand: 'pnpm add @tensorflow/tfjs-node',
77
- reason: 'Required for machine learning model training and inference'
78
- });
75
+ requirePluginDependency('ml-plugin');
79
76
 
80
77
  // Model instances
81
78
  this.models = {};
@@ -139,7 +136,7 @@ export class MLPlugin extends Plugin {
139
136
  console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
140
137
  }
141
138
 
142
- this.emit('installed', {
139
+ this.emit('db:plugin:installed', {
143
140
  plugin: 'MLPlugin',
144
141
  models: Object.keys(this.models)
145
142
  });
@@ -591,6 +588,29 @@ export class MLPlugin extends Plugin {
591
588
  data = allData;
592
589
  }
593
590
 
591
+ // Apply custom filter function if provided
592
+ if (modelConfig.filter && typeof modelConfig.filter === 'function') {
593
+ if (this.config.verbose) {
594
+ console.log(`[MLPlugin] Applying custom filter function...`);
595
+ }
596
+
597
+ const originalLength = data.length;
598
+ data = data.filter(modelConfig.filter);
599
+
600
+ if (this.config.verbose) {
601
+ console.log(`[MLPlugin] Filter reduced dataset from ${originalLength} to ${data.length} samples`);
602
+ }
603
+ }
604
+
605
+ // Apply custom map function if provided
606
+ if (modelConfig.map && typeof modelConfig.map === 'function') {
607
+ if (this.config.verbose) {
608
+ console.log(`[MLPlugin] Applying custom map function...`);
609
+ }
610
+
611
+ data = data.map(modelConfig.map);
612
+ }
613
+
594
614
  if (!data || data.length < this.config.minTrainingSamples) {
595
615
  throw new TrainingError(
596
616
  `Insufficient training data: ${data?.length || 0} samples (minimum: ${this.config.minTrainingSamples})`,
@@ -629,7 +649,7 @@ export class MLPlugin extends Plugin {
629
649
  console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
630
650
  }
631
651
 
632
- this.emit('modelTrained', {
652
+ this.emit('plg:ml:model-trained', {
633
653
  modelName,
634
654
  type: modelConfig.type,
635
655
  result
@@ -671,7 +691,7 @@ export class MLPlugin extends Plugin {
671
691
  const result = await model.predict(input);
672
692
  this.stats.totalPredictions++;
673
693
 
674
- this.emit('prediction', {
694
+ this.emit('plg:ml:prediction', {
675
695
  modelName,
676
696
  input,
677
697
  result
@@ -807,7 +827,11 @@ export class MLPlugin extends Plugin {
807
827
  async _initializeVersioning(modelName) {
808
828
  try {
809
829
  const storage = this.getStorage();
810
- const [ok, err, versionInfo] = await tryFn(() => storage.get(`version_${modelName}`));
830
+ const modelConfig = this.config.models[modelName];
831
+ const resourceName = modelConfig.resource;
832
+ const [ok, err, versionInfo] = await tryFn(() =>
833
+ storage.get(storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'))
834
+ );
811
835
 
812
836
  if (ok && versionInfo) {
813
837
  // Load existing version info
@@ -853,6 +877,8 @@ export class MLPlugin extends Plugin {
853
877
  async _updateVersionInfo(modelName, version) {
854
878
  try {
855
879
  const storage = this.getStorage();
880
+ const modelConfig = this.config.models[modelName];
881
+ const resourceName = modelConfig.resource;
856
882
  const versionInfo = this.modelVersions.get(modelName) || { currentVersion: 1, latestVersion: 0 };
857
883
 
858
884
  versionInfo.latestVersion = Math.max(versionInfo.latestVersion, version);
@@ -860,12 +886,16 @@ export class MLPlugin extends Plugin {
860
886
 
861
887
  this.modelVersions.set(modelName, versionInfo);
862
888
 
863
- await storage.patch(`version_${modelName}`, {
864
- modelName,
865
- currentVersion: versionInfo.currentVersion,
866
- latestVersion: versionInfo.latestVersion,
867
- updatedAt: new Date().toISOString()
868
- });
889
+ await storage.set(
890
+ storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'),
891
+ {
892
+ modelName,
893
+ currentVersion: versionInfo.currentVersion,
894
+ latestVersion: versionInfo.latestVersion,
895
+ updatedAt: new Date().toISOString()
896
+ },
897
+ { behavior: 'body-overflow' }
898
+ );
869
899
 
870
900
  if (this.config.verbose) {
871
901
  console.log(`[MLPlugin] Updated version info for "${modelName}": current=v${versionInfo.currentVersion}, latest=v${versionInfo.latestVersion}`);
@@ -882,6 +912,8 @@ export class MLPlugin extends Plugin {
882
912
  async _saveModel(modelName) {
883
913
  try {
884
914
  const storage = this.getStorage();
915
+ const modelConfig = this.config.models[modelName];
916
+ const resourceName = modelConfig.resource;
885
917
  const exportedModel = await this.models[modelName].export();
886
918
 
887
919
  if (!exportedModel) {
@@ -898,45 +930,57 @@ export class MLPlugin extends Plugin {
898
930
  const version = this._getNextVersion(modelName);
899
931
  const modelStats = this.models[modelName].getStats();
900
932
 
901
- // Save versioned model
902
- await storage.patch(`model_${modelName}_v${version}`, {
903
- modelName,
904
- version,
905
- type: 'model',
906
- data: JSON.stringify(exportedModel),
907
- metrics: JSON.stringify({
908
- loss: modelStats.loss,
909
- accuracy: modelStats.accuracy,
910
- samples: modelStats.samples
911
- }),
912
- savedAt: new Date().toISOString()
913
- });
933
+ // Save versioned model binary to S3 body
934
+ await storage.set(
935
+ storage.getPluginKey(resourceName, 'models', modelName, `v${version}`),
936
+ {
937
+ modelName,
938
+ version,
939
+ type: 'model',
940
+ modelData: exportedModel, // TensorFlow.js model object (will go to body)
941
+ metrics: {
942
+ loss: modelStats.loss,
943
+ accuracy: modelStats.accuracy,
944
+ samples: modelStats.samples
945
+ },
946
+ savedAt: new Date().toISOString()
947
+ },
948
+ { behavior: 'body-only' } // Large binary data goes to S3 body
949
+ );
914
950
 
915
951
  // Update version info
916
952
  await this._updateVersionInfo(modelName, version);
917
953
 
918
954
  // Save active reference (points to current version)
919
- await storage.patch(`model_${modelName}_active`, {
920
- modelName,
921
- version,
922
- type: 'reference',
923
- updatedAt: new Date().toISOString()
924
- });
955
+ await storage.set(
956
+ storage.getPluginKey(resourceName, 'metadata', modelName, 'active'),
957
+ {
958
+ modelName,
959
+ version,
960
+ type: 'reference',
961
+ updatedAt: new Date().toISOString()
962
+ },
963
+ { behavior: 'body-overflow' } // Small metadata
964
+ );
925
965
 
926
966
  if (this.config.verbose) {
927
- console.log(`[MLPlugin] Saved model "${modelName}" v${version} to plugin storage (S3)`);
967
+ console.log(`[MLPlugin] Saved model "${modelName}" v${version} to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
928
968
  }
929
969
  } else {
930
970
  // Save without versioning (legacy behavior)
931
- await storage.patch(`model_${modelName}`, {
932
- modelName,
933
- type: 'model',
934
- data: JSON.stringify(exportedModel),
935
- savedAt: new Date().toISOString()
936
- });
971
+ await storage.set(
972
+ storage.getPluginKey(resourceName, 'models', modelName, 'latest'),
973
+ {
974
+ modelName,
975
+ type: 'model',
976
+ modelData: exportedModel,
977
+ savedAt: new Date().toISOString()
978
+ },
979
+ { behavior: 'body-only' }
980
+ );
937
981
 
938
982
  if (this.config.verbose) {
939
- console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage (S3)`);
983
+ console.log(`[MLPlugin] Saved model "${modelName}" to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
940
984
  }
941
985
  }
942
986
  } catch (error) {
@@ -945,7 +989,7 @@ export class MLPlugin extends Plugin {
945
989
  }
946
990
 
947
991
  /**
948
- * Save intermediate training data to plugin storage (incremental)
992
+ * Save intermediate training data to plugin storage (incremental - only new samples)
949
993
  * @private
950
994
  */
951
995
  async _saveTrainingData(modelName, rawData) {
@@ -953,76 +997,117 @@ export class MLPlugin extends Plugin {
953
997
  const storage = this.getStorage();
954
998
  const model = this.models[modelName];
955
999
  const modelConfig = this.config.models[modelName];
1000
+ const resourceName = modelConfig.resource;
956
1001
  const modelStats = model.getStats();
957
1002
  const enableVersioning = this.config.enableVersioning;
958
1003
 
959
1004
  // Extract features and target from raw data
960
- const trainingEntry = {
961
- version: enableVersioning ? this.modelVersions.get(modelName)?.latestVersion || 1 : undefined,
962
- samples: rawData.length,
963
- features: modelConfig.features,
964
- target: modelConfig.target,
965
- data: rawData.map(item => {
966
- const features = {};
967
- modelConfig.features.forEach(feature => {
968
- features[feature] = item[feature];
969
- });
970
- return {
971
- features,
972
- target: item[modelConfig.target]
973
- };
974
- }),
975
- metrics: {
976
- loss: modelStats.loss,
977
- accuracy: modelStats.accuracy,
978
- r2: modelStats.r2
979
- },
980
- trainedAt: new Date().toISOString()
981
- };
1005
+ const processedData = rawData.map(item => {
1006
+ const features = {};
1007
+ modelConfig.features.forEach(feature => {
1008
+ features[feature] = item[feature];
1009
+ });
1010
+ return {
1011
+ id: item.id || `${Date.now()}_${Math.random()}`, // Use record ID or generate
1012
+ features,
1013
+ target: item[modelConfig.target]
1014
+ };
1015
+ });
982
1016
 
983
1017
  if (enableVersioning) {
984
- // Incremental: Load existing history and append
985
- const [ok, err, existing] = await tryFn(() => storage.get(`training_history_${modelName}`));
1018
+ const version = this._getNextVersion(modelName);
1019
+
1020
+ // Load existing history to calculate incremental data
1021
+ const [ok, err, existing] = await tryFn(() =>
1022
+ storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
1023
+ );
986
1024
 
987
1025
  let history = [];
1026
+ let previousSampleIds = new Set();
1027
+
988
1028
  if (ok && existing && existing.history) {
989
- try {
990
- history = JSON.parse(existing.history);
991
- } catch (e) {
992
- history = [];
993
- }
1029
+ history = existing.history;
1030
+ // Collect all IDs from previous versions
1031
+ history.forEach(entry => {
1032
+ if (entry.sampleIds) {
1033
+ entry.sampleIds.forEach(id => previousSampleIds.add(id));
1034
+ }
1035
+ });
994
1036
  }
995
1037
 
996
- // Append new entry
997
- history.push(trainingEntry);
1038
+ // Detect new samples (not in previous versions)
1039
+ const currentSampleIds = new Set(processedData.map(d => d.id));
1040
+ const newSamples = processedData.filter(d => !previousSampleIds.has(d.id));
1041
+ const newSampleIds = newSamples.map(d => d.id);
1042
+
1043
+ // Save only NEW samples to S3 body (incremental)
1044
+ if (newSamples.length > 0) {
1045
+ await storage.set(
1046
+ storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${version}`),
1047
+ {
1048
+ modelName,
1049
+ version,
1050
+ samples: newSamples, // Only new samples
1051
+ features: modelConfig.features,
1052
+ target: modelConfig.target,
1053
+ savedAt: new Date().toISOString()
1054
+ },
1055
+ { behavior: 'body-only' } // Dataset goes to S3 body
1056
+ );
1057
+ }
998
1058
 
999
- // Save updated history
1000
- await storage.patch(`training_history_${modelName}`, {
1001
- modelName,
1002
- type: 'training_history',
1003
- totalTrainings: history.length,
1004
- latestVersion: trainingEntry.version,
1005
- history: JSON.stringify(history),
1006
- updatedAt: new Date().toISOString()
1007
- });
1059
+ // Append metadata to history (no full dataset duplication)
1060
+ const historyEntry = {
1061
+ version,
1062
+ totalSamples: processedData.length, // Total cumulative
1063
+ newSamples: newSamples.length, // Only new in this version
1064
+ sampleIds: Array.from(currentSampleIds), // All IDs for this version
1065
+ newSampleIds, // IDs of new samples
1066
+ storageKey: newSamples.length > 0 ? `training/data/${modelName}/v${version}` : null,
1067
+ metrics: {
1068
+ loss: modelStats.loss,
1069
+ accuracy: modelStats.accuracy,
1070
+ r2: modelStats.r2
1071
+ },
1072
+ trainedAt: new Date().toISOString()
1073
+ };
1074
+
1075
+ history.push(historyEntry);
1076
+
1077
+ // Save updated history (metadata only, no full datasets)
1078
+ await storage.set(
1079
+ storage.getPluginKey(resourceName, 'training', 'history', modelName),
1080
+ {
1081
+ modelName,
1082
+ type: 'training_history',
1083
+ totalTrainings: history.length,
1084
+ latestVersion: version,
1085
+ history, // Array of metadata entries (not full data)
1086
+ updatedAt: new Date().toISOString()
1087
+ },
1088
+ { behavior: 'body-overflow' } // History metadata
1089
+ );
1008
1090
 
1009
1091
  if (this.config.verbose) {
1010
- console.log(`[MLPlugin] Appended training data for "${modelName}" v${trainingEntry.version} (${trainingEntry.samples} samples, total: ${history.length} trainings)`);
1092
+ console.log(`[MLPlugin] Saved training data for "${modelName}" v${version}: ${newSamples.length} new samples (total: ${processedData.length}, storage: resource=${resourceName}/plugin=ml/training/data/${modelName}/v${version})`);
1011
1093
  }
1012
1094
  } else {
1013
1095
  // Legacy: Replace training data (non-incremental)
1014
- await storage.patch(`training_data_${modelName}`, {
1015
- modelName,
1016
- type: 'training_data',
1017
- samples: trainingEntry.samples,
1018
- features: JSON.stringify(trainingEntry.features),
1019
- target: trainingEntry.target,
1020
- data: JSON.stringify(trainingEntry.data),
1021
- savedAt: trainingEntry.trainedAt
1022
- });
1096
+ await storage.set(
1097
+ storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'),
1098
+ {
1099
+ modelName,
1100
+ type: 'training_data',
1101
+ samples: processedData,
1102
+ features: modelConfig.features,
1103
+ target: modelConfig.target,
1104
+ savedAt: new Date().toISOString()
1105
+ },
1106
+ { behavior: 'body-only' }
1107
+ );
1023
1108
 
1024
1109
  if (this.config.verbose) {
1025
- console.log(`[MLPlugin] Saved training data for "${modelName}" (${trainingEntry.samples} samples) to plugin storage (S3)`);
1110
+ console.log(`[MLPlugin] Saved training data for "${modelName}" (${processedData.length} samples) to S3 (resource=${resourceName}/plugin=ml/training/data/${modelName}/latest)`);
1026
1111
  }
1027
1112
  }
1028
1113
  } catch (error) {
@@ -1037,23 +1122,28 @@ export class MLPlugin extends Plugin {
1037
1122
  async _loadModel(modelName) {
1038
1123
  try {
1039
1124
  const storage = this.getStorage();
1125
+ const modelConfig = this.config.models[modelName];
1126
+ const resourceName = modelConfig.resource;
1040
1127
  const enableVersioning = this.config.enableVersioning;
1041
1128
 
1042
1129
  if (enableVersioning) {
1043
- // Load active version
1044
- const [okRef, errRef, activeRef] = await tryFn(() => storage.get(`model_${modelName}_active`));
1130
+ // Load active version reference
1131
+ const [okRef, errRef, activeRef] = await tryFn(() =>
1132
+ storage.get(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'))
1133
+ );
1045
1134
 
1046
1135
  if (okRef && activeRef && activeRef.version) {
1047
1136
  // Load the active version
1048
1137
  const version = activeRef.version;
1049
- const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
1138
+ const [ok, err, versionData] = await tryFn(() =>
1139
+ storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`))
1140
+ );
1050
1141
 
1051
- if (ok && versionData) {
1052
- const modelData = JSON.parse(versionData.data);
1053
- await this.models[modelName].import(modelData);
1142
+ if (ok && versionData && versionData.modelData) {
1143
+ await this.models[modelName].import(versionData.modelData);
1054
1144
 
1055
1145
  if (this.config.verbose) {
1056
- console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from plugin storage (S3)`);
1146
+ console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
1057
1147
  }
1058
1148
  return;
1059
1149
  }
@@ -1063,14 +1153,15 @@ export class MLPlugin extends Plugin {
1063
1153
  const versionInfo = this.modelVersions.get(modelName);
1064
1154
  if (versionInfo && versionInfo.latestVersion > 0) {
1065
1155
  const version = versionInfo.latestVersion;
1066
- const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
1156
+ const [ok, err, versionData] = await tryFn(() =>
1157
+ storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`))
1158
+ );
1067
1159
 
1068
- if (ok && versionData) {
1069
- const modelData = JSON.parse(versionData.data);
1070
- await this.models[modelName].import(modelData);
1160
+ if (ok && versionData && versionData.modelData) {
1161
+ await this.models[modelName].import(versionData.modelData);
1071
1162
 
1072
1163
  if (this.config.verbose) {
1073
- console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from plugin storage (S3)`);
1164
+ console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from S3`);
1074
1165
  }
1075
1166
  return;
1076
1167
  }
@@ -1081,20 +1172,21 @@ export class MLPlugin extends Plugin {
1081
1172
  }
1082
1173
  } else {
1083
1174
  // Legacy: Load non-versioned model
1084
- const [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
1175
+ const [ok, err, record] = await tryFn(() =>
1176
+ storage.get(storage.getPluginKey(resourceName, 'models', modelName, 'latest'))
1177
+ );
1085
1178
 
1086
- if (!ok || !record) {
1179
+ if (!ok || !record || !record.modelData) {
1087
1180
  if (this.config.verbose) {
1088
1181
  console.log(`[MLPlugin] No saved model found for "${modelName}"`);
1089
1182
  }
1090
1183
  return;
1091
1184
  }
1092
1185
 
1093
- const modelData = JSON.parse(record.data);
1094
- await this.models[modelName].import(modelData);
1186
+ await this.models[modelName].import(record.modelData);
1095
1187
 
1096
1188
  if (this.config.verbose) {
1097
- console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage (S3)`);
1189
+ console.log(`[MLPlugin] Loaded model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
1098
1190
  }
1099
1191
  }
1100
1192
  } catch (error) {
@@ -1103,29 +1195,82 @@ export class MLPlugin extends Plugin {
1103
1195
  }
1104
1196
 
1105
1197
  /**
1106
- * Load training data from plugin storage
1198
+ * Load training data from plugin storage (reconstructs specific version from incremental data)
1107
1199
  * @param {string} modelName - Model name
1200
+ * @param {number} version - Version number (optional, defaults to latest)
1108
1201
  * @returns {Object|null} Training data or null if not found
1109
1202
  */
1110
- async getTrainingData(modelName) {
1203
+ async getTrainingData(modelName, version = null) {
1111
1204
  try {
1112
1205
  const storage = this.getStorage();
1113
- const [ok, err, record] = await tryFn(() => storage.get(`training_data_${modelName}`));
1206
+ const modelConfig = this.config.models[modelName];
1207
+ const resourceName = modelConfig.resource;
1208
+ const enableVersioning = this.config.enableVersioning;
1209
+
1210
+ if (!enableVersioning) {
1211
+ // Legacy: Load non-versioned training data
1212
+ const [ok, err, record] = await tryFn(() =>
1213
+ storage.get(storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'))
1214
+ );
1215
+
1216
+ if (!ok || !record) {
1217
+ if (this.config.verbose) {
1218
+ console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
1219
+ }
1220
+ return null;
1221
+ }
1114
1222
 
1115
- if (!ok || !record) {
1223
+ return {
1224
+ modelName: record.modelName,
1225
+ samples: record.samples,
1226
+ features: record.features,
1227
+ target: record.target,
1228
+ data: record.samples,
1229
+ savedAt: record.savedAt
1230
+ };
1231
+ }
1232
+
1233
+ // Versioned: Reconstruct dataset from incremental versions
1234
+ const [okHistory, errHistory, historyData] = await tryFn(() =>
1235
+ storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
1236
+ );
1237
+
1238
+ if (!okHistory || !historyData || !historyData.history) {
1116
1239
  if (this.config.verbose) {
1117
- console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
1240
+ console.log(`[MLPlugin] No training history found for "${modelName}"`);
1118
1241
  }
1119
1242
  return null;
1120
1243
  }
1121
1244
 
1245
+ const targetVersion = version || historyData.latestVersion;
1246
+ const reconstructedSamples = [];
1247
+
1248
+ // Load and combine all versions up to target version
1249
+ for (const entry of historyData.history) {
1250
+ if (entry.version > targetVersion) break;
1251
+
1252
+ if (entry.storageKey && entry.newSamples > 0) {
1253
+ const [ok, err, versionData] = await tryFn(() =>
1254
+ storage.get(storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${entry.version}`))
1255
+ );
1256
+
1257
+ if (ok && versionData && versionData.samples) {
1258
+ reconstructedSamples.push(...versionData.samples);
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ const targetEntry = historyData.history.find(e => e.version === targetVersion);
1264
+
1122
1265
  return {
1123
- modelName: record.modelName,
1124
- samples: record.samples,
1125
- features: JSON.parse(record.features),
1126
- target: record.target,
1127
- data: JSON.parse(record.data),
1128
- savedAt: record.savedAt
1266
+ modelName,
1267
+ version: targetVersion,
1268
+ samples: reconstructedSamples,
1269
+ totalSamples: reconstructedSamples.length,
1270
+ features: modelConfig.features,
1271
+ target: modelConfig.target,
1272
+ metrics: targetEntry?.metrics,
1273
+ savedAt: targetEntry?.trainedAt
1129
1274
  };
1130
1275
  } catch (error) {
1131
1276
  console.error(`[MLPlugin] Failed to load training data for "${modelName}":`, error.message);
@@ -1134,16 +1279,35 @@ export class MLPlugin extends Plugin {
1134
1279
  }
1135
1280
 
1136
1281
  /**
1137
- * Delete model from plugin storage
1282
+ * Delete model from plugin storage (all versions)
1138
1283
  * @private
1139
1284
  */
1140
1285
  async _deleteModel(modelName) {
1141
1286
  try {
1142
1287
  const storage = this.getStorage();
1143
- await storage.delete(`model_${modelName}`);
1288
+ const modelConfig = this.config.models[modelName];
1289
+ const resourceName = modelConfig.resource;
1290
+ const enableVersioning = this.config.enableVersioning;
1291
+
1292
+ if (enableVersioning) {
1293
+ // Delete all versions
1294
+ const versionInfo = this.modelVersions.get(modelName);
1295
+ if (versionInfo && versionInfo.latestVersion > 0) {
1296
+ for (let v = 1; v <= versionInfo.latestVersion; v++) {
1297
+ await storage.delete(storage.getPluginKey(resourceName, 'models', modelName, `v${v}`));
1298
+ }
1299
+ }
1300
+
1301
+ // Delete metadata
1302
+ await storage.delete(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'));
1303
+ await storage.delete(storage.getPluginKey(resourceName, 'metadata', modelName, 'versions'));
1304
+ } else {
1305
+ // Delete non-versioned model
1306
+ await storage.delete(storage.getPluginKey(resourceName, 'models', modelName, 'latest'));
1307
+ }
1144
1308
 
1145
1309
  if (this.config.verbose) {
1146
- console.log(`[MLPlugin] Deleted model "${modelName}" from plugin storage`);
1310
+ console.log(`[MLPlugin] Deleted model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/)`);
1147
1311
  }
1148
1312
  } catch (error) {
1149
1313
  // Ignore errors (model might not exist)
@@ -1154,16 +1318,39 @@ export class MLPlugin extends Plugin {
1154
1318
  }
1155
1319
 
1156
1320
  /**
1157
- * Delete training data from plugin storage
1321
+ * Delete training data from plugin storage (all versions)
1158
1322
  * @private
1159
1323
  */
1160
1324
  async _deleteTrainingData(modelName) {
1161
1325
  try {
1162
1326
  const storage = this.getStorage();
1163
- await storage.delete(`training_data_${modelName}`);
1327
+ const modelConfig = this.config.models[modelName];
1328
+ const resourceName = modelConfig.resource;
1329
+ const enableVersioning = this.config.enableVersioning;
1330
+
1331
+ if (enableVersioning) {
1332
+ // Delete all version data
1333
+ const [ok, err, historyData] = await tryFn(() =>
1334
+ storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName))
1335
+ );
1336
+
1337
+ if (ok && historyData && historyData.history) {
1338
+ for (const entry of historyData.history) {
1339
+ if (entry.storageKey) {
1340
+ await storage.delete(storage.getPluginKey(resourceName, 'training', 'data', modelName, `v${entry.version}`));
1341
+ }
1342
+ }
1343
+ }
1344
+
1345
+ // Delete history
1346
+ await storage.delete(storage.getPluginKey(resourceName, 'training', 'history', modelName));
1347
+ } else {
1348
+ // Delete non-versioned training data
1349
+ await storage.delete(storage.getPluginKey(resourceName, 'training', 'data', modelName, 'latest'));
1350
+ }
1164
1351
 
1165
1352
  if (this.config.verbose) {
1166
- console.log(`[MLPlugin] Deleted training data for "${modelName}" from plugin storage`);
1353
+ console.log(`[MLPlugin] Deleted training data for "${modelName}" from S3 (resource=${resourceName}/plugin=ml/training/)`);
1167
1354
  }
1168
1355
  } catch (error) {
1169
1356
  // Ignore errors (training data might not exist)
@@ -1185,20 +1372,21 @@ export class MLPlugin extends Plugin {
1185
1372
 
1186
1373
  try {
1187
1374
  const storage = this.getStorage();
1375
+ const modelConfig = this.config.models[modelName];
1376
+ const resourceName = modelConfig.resource;
1188
1377
  const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
1189
1378
  const versions = [];
1190
1379
 
1191
1380
  // Load each version
1192
1381
  for (let v = 1; v <= versionInfo.latestVersion; v++) {
1193
- const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${v}`));
1382
+ const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${v}`)));
1194
1383
 
1195
1384
  if (ok && versionData) {
1196
- const metrics = versionData.metrics ? JSON.parse(versionData.metrics) : {};
1197
1385
  versions.push({
1198
1386
  version: v,
1199
1387
  savedAt: versionData.savedAt,
1200
1388
  isCurrent: v === versionInfo.currentVersion,
1201
- metrics
1389
+ metrics: versionData.metrics
1202
1390
  });
1203
1391
  }
1204
1392
  }
@@ -1226,14 +1414,19 @@ export class MLPlugin extends Plugin {
1226
1414
 
1227
1415
  try {
1228
1416
  const storage = this.getStorage();
1229
- const [ok, err, versionData] = await tryFn(() => storage.get(`model_${modelName}_v${version}`));
1417
+ const modelConfig = this.config.models[modelName];
1418
+ const resourceName = modelConfig.resource;
1419
+ const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version}`)));
1230
1420
 
1231
1421
  if (!ok || !versionData) {
1232
1422
  throw new MLError(`Version ${version} not found for model "${modelName}"`, { modelName, version });
1233
1423
  }
1234
1424
 
1235
- const modelData = JSON.parse(versionData.data);
1236
- await this.models[modelName].import(modelData);
1425
+ if (!versionData.modelData) {
1426
+ throw new MLError(`Model data not found in version ${version}`, { modelName, version });
1427
+ }
1428
+
1429
+ await this.models[modelName].import(versionData.modelData);
1237
1430
 
1238
1431
  // Update current version in memory (don't save to storage yet)
1239
1432
  const versionInfo = this.modelVersions.get(modelName);
@@ -1267,6 +1460,9 @@ export class MLPlugin extends Plugin {
1267
1460
  throw new MLError('Versioning is not enabled', { modelName });
1268
1461
  }
1269
1462
 
1463
+ const modelConfig = this.config.models[modelName];
1464
+ const resourceName = modelConfig.resource;
1465
+
1270
1466
  // Load the version into the model
1271
1467
  await this.loadModelVersion(modelName, version);
1272
1468
 
@@ -1275,7 +1471,7 @@ export class MLPlugin extends Plugin {
1275
1471
 
1276
1472
  // Update active reference
1277
1473
  const storage = this.getStorage();
1278
- await storage.patch(`model_${modelName}_active`, {
1474
+ await storage.set(storage.getPluginKey(resourceName, 'metadata', modelName, 'active'), {
1279
1475
  modelName,
1280
1476
  version,
1281
1477
  type: 'reference',
@@ -1302,7 +1498,9 @@ export class MLPlugin extends Plugin {
1302
1498
 
1303
1499
  try {
1304
1500
  const storage = this.getStorage();
1305
- const [ok, err, historyData] = await tryFn(() => storage.get(`training_history_${modelName}`));
1501
+ const modelConfig = this.config.models[modelName];
1502
+ const resourceName = modelConfig.resource;
1503
+ const [ok, err, historyData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'training', 'history', modelName)));
1306
1504
 
1307
1505
  if (!ok || !historyData) {
1308
1506
  return null;
@@ -1335,9 +1533,11 @@ export class MLPlugin extends Plugin {
1335
1533
 
1336
1534
  try {
1337
1535
  const storage = this.getStorage();
1536
+ const modelConfig = this.config.models[modelName];
1537
+ const resourceName = modelConfig.resource;
1338
1538
 
1339
- const [ok1, err1, v1Data] = await tryFn(() => storage.get(`model_${modelName}_v${version1}`));
1340
- const [ok2, err2, v2Data] = await tryFn(() => storage.get(`model_${modelName}_v${version2}`));
1539
+ const [ok1, err1, v1Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version1}`)));
1540
+ const [ok2, err2, v2Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, 'models', modelName, `v${version2}`)));
1341
1541
 
1342
1542
  if (!ok1 || !v1Data) {
1343
1543
  throw new MLError(`Version ${version1} not found`, { modelName, version: version1 });