@mxf-dev/core 2.0.0 → 2.0.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.
Files changed (31) hide show
  1. package/dist/protocols/mcp/providers/OpenRouterMcpClient.d.ts +34 -4
  2. package/dist/protocols/mcp/providers/OpenRouterMcpClient.d.ts.map +1 -1
  3. package/dist/protocols/mcp/providers/OpenRouterMcpClient.js +297 -65
  4. package/dist/protocols/mcp/providers/OpenRouterMcpClient.js.map +1 -1
  5. package/dist/protocols/mcp/utils/NetworkRecovery.d.ts +21 -0
  6. package/dist/protocols/mcp/utils/NetworkRecovery.d.ts.map +1 -1
  7. package/dist/protocols/mcp/utils/NetworkRecovery.js +62 -2
  8. package/dist/protocols/mcp/utils/NetworkRecovery.js.map +1 -1
  9. package/dist/services/MemoryService.d.ts +17 -1
  10. package/dist/services/MemoryService.d.ts.map +1 -1
  11. package/dist/services/MemoryService.js +33 -13
  12. package/dist/services/MemoryService.js.map +1 -1
  13. package/dist/services/MxfMLService.d.ts +5 -0
  14. package/dist/services/MxfMLService.d.ts.map +1 -1
  15. package/dist/services/MxfMLService.js +32 -6
  16. package/dist/services/MxfMLService.js.map +1 -1
  17. package/dist/services/PredictiveAnalyticsService.d.ts +11 -5
  18. package/dist/services/PredictiveAnalyticsService.d.ts.map +1 -1
  19. package/dist/services/PredictiveAnalyticsService.js +42 -20
  20. package/dist/services/PredictiveAnalyticsService.js.map +1 -1
  21. package/dist/types/NetworkRecoveryTypes.d.ts +1 -0
  22. package/dist/types/NetworkRecoveryTypes.d.ts.map +1 -1
  23. package/dist/types/NetworkRecoveryTypes.js +14 -0
  24. package/dist/types/NetworkRecoveryTypes.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/protocols/mcp/providers/OpenRouterMcpClient.ts +351 -83
  27. package/src/protocols/mcp/utils/NetworkRecovery.ts +74 -3
  28. package/src/services/MemoryService.ts +40 -16
  29. package/src/services/MxfMLService.ts +41 -7
  30. package/src/services/PredictiveAnalyticsService.ts +47 -20
  31. package/src/types/NetworkRecoveryTypes.ts +17 -1
@@ -552,7 +552,14 @@ export class MemoryService {
552
552
  // Handle agent memory UPDATE
553
553
  if (typeof memoryEventData.id === 'string' && memoryEventData.data) {
554
554
  // CRITICAL: Must subscribe to Observable to trigger execution!
555
- this.updateAgentMemory(memoryEventData.id, memoryEventData.data).subscribe({
555
+ // The request context routes the UPDATE_RESULT back to the
556
+ // requesting agent with its own operationId — without it the
557
+ // SDK's pending save can never observe its result.
558
+ this.updateAgentMemory(memoryEventData.id, memoryEventData.data, {
559
+ operationId: memoryEventData.operationId,
560
+ requesterAgentId: event.agentId,
561
+ requesterChannelId: event.channelId
562
+ }).subscribe({
556
563
  next: () => null,
557
564
  error: (err) => this.logger.error(`Agent memory update failed for ${memoryEventData.id}: ${err}`)
558
565
  });
@@ -702,11 +709,27 @@ export class MemoryService {
702
709
 
703
710
  /**
704
711
  * Update agent memory
712
+ *
713
+ * When the update was requested over the wire (SDK → Events.Memory.UPDATE),
714
+ * requestContext carries the requester's operationId and identity. The
715
+ * UPDATE_RESULT emitted here must echo that operationId and be addressed to
716
+ * the requesting agent: the SDK matches results by operationId, and event
717
+ * forwarding routes them by payload.agentId. Before this parameter existed,
718
+ * agent-scope results were emitted with a freshly generated operationId under
719
+ * SYSTEM_AGENT — no SDK save round-trip could ever observe its own result, so
720
+ * every awaited save hung forever and every fire-and-forget save leaked its
721
+ * result listener.
722
+ *
705
723
  * @param pAgentId Agent ID
706
724
  * @param updates Memory updates
725
+ * @param requestContext Requester identity for result routing (wire requests only)
707
726
  * @returns Observable of updated agent memory
708
727
  */
709
- public updateAgentMemory(pAgentId: string, updates: Partial<IEnhancedAgentMemory>): Observable<IEnhancedAgentMemory> {
728
+ public updateAgentMemory(
729
+ pAgentId: string,
730
+ updates: Partial<IEnhancedAgentMemory>,
731
+ requestContext?: { operationId: string; requesterAgentId: AgentId; requesterChannelId: ChannelId }
732
+ ): Observable<IEnhancedAgentMemory> {
710
733
  this.validator.assertIsNonEmptyString(pAgentId, 'Agent ID must be a non-empty string');
711
734
  this.validator.assertIsObject(updates, 'Updates must be an object');
712
735
 
@@ -777,8 +800,11 @@ export class MemoryService {
777
800
  }
778
801
  }
779
802
 
780
- // Emit update event through EventBus
781
- const operationId = uuidv4(); // Generate operationId for the event data
803
+ // Emit update event through EventBus. Echo the requester's
804
+ // operationId and address the result to the requesting agent so
805
+ // the SDK's pending save resolves; internal callers (no
806
+ // requestContext) keep the system-level identity.
807
+ const operationId = requestContext?.operationId ?? uuidv4();
782
808
  const updateResultData: MemoryUpdateResultEventData = {
783
809
  operationId,
784
810
  scope: MemoryScope.AGENT,
@@ -786,14 +812,13 @@ export class MemoryService {
786
812
  memory: updatedMemory
787
813
  };
788
814
 
789
- // Define standard agentId and channelId for system-level memory events
790
- const systemAgentId: AgentId = 'SYSTEM_AGENT'; // Or a more specific system agent ID
791
- const noChannelId: ChannelId = 'NO_CHANNEL'; // Or a more specific system channel ID or null if appropriate
815
+ const resultAgentId: AgentId = requestContext?.requesterAgentId ?? 'SYSTEM_AGENT';
816
+ const resultChannelId: ChannelId = requestContext?.requesterChannelId ?? 'NO_CHANNEL';
792
817
 
793
818
  const payload = createMemoryUpdateResultEventPayload(
794
819
  Events.Memory.UPDATE_RESULT,
795
- systemAgentId,
796
- noChannelId,
820
+ resultAgentId,
821
+ resultChannelId,
797
822
  updateResultData
798
823
  );
799
824
  EventBus.server.emit(Events.Memory.UPDATE_RESULT, payload);
@@ -801,22 +826,21 @@ export class MemoryService {
801
826
  observer.next(updatedMemory);
802
827
  observer.complete();
803
828
  }, error => {
804
- const operationId_agent_update_error = uuidv4();
829
+ // Errors must reach the requester too — a save whose failure is
830
+ // only logged server-side leaves the requesting SDK waiting forever.
805
831
  const updateResultData_agent_update_error: MemoryUpdateResultEventData = {
806
- operationId: operationId_agent_update_error,
832
+ operationId: requestContext?.operationId ?? uuidv4(),
807
833
  scope: MemoryScope.AGENT,
808
834
  id: pAgentId,
809
835
  memory: null,
810
836
  error: (error as Error).message
811
837
  };
812
- const systemAgentId_agent_update_error: AgentId = 'SYSTEM_AGENT'; // Or a more specific system agent ID
813
- const noChannelId_agent_update_error: ChannelId = 'NO_CHANNEL'; // Or a more specific system channel ID or null if appropriate
814
838
  EventBus.server.emit(
815
- Events.Memory.UPDATE_RESULT,
839
+ Events.Memory.UPDATE_RESULT,
816
840
  createMemoryUpdateResultEventPayload(
817
841
  Events.Memory.UPDATE_RESULT,
818
- systemAgentId_agent_update_error,
819
- noChannelId_agent_update_error,
842
+ requestContext?.requesterAgentId ?? 'SYSTEM_AGENT',
843
+ requestContext?.requesterChannelId ?? 'NO_CHANNEL',
820
844
  updateResultData_agent_update_error
821
845
  )
822
846
  );
@@ -480,6 +480,16 @@ export class MxfMLService {
480
480
  throw new Error(`[MxfMLService] Model "${modelId}" has not been built yet`);
481
481
  }
482
482
 
483
+ // Fail fast on concurrent training: TF.js rejects overlapping fit() calls
484
+ // on a single model, and two callers would race on this entry's status
485
+ // and metrics. Callers must serialize training per model.
486
+ if (entry.status === ModelStatus.TRAINING) {
487
+ throw new Error(
488
+ `[MxfMLService] Model "${modelId}" is already training — ` +
489
+ `concurrent training on the same model is not allowed`
490
+ );
491
+ }
492
+
483
493
  if (xData.length < entry.config.minTrainingSamples) {
484
494
  throw new Error(
485
495
  `[MxfMLService] Insufficient training data for "${modelId}": ` +
@@ -497,10 +507,14 @@ export class MxfMLService {
497
507
  entry.status = ModelStatus.TRAINING;
498
508
  const startTime = Date.now();
499
509
 
510
+ // fit() is async, so tf.tidy() cannot wrap it — create the tensors
511
+ // eagerly and dispose them in finally so a failed fit() does not leak.
512
+ let xs: import('@tensorflow/tfjs').Tensor2D | undefined;
513
+ let ys: import('@tensorflow/tfjs').Tensor2D | undefined;
514
+
500
515
  try {
501
- // Create tensors inside tidy to prevent leaks during training setup
502
- const xs = tfRef.tensor2d(xData);
503
- const ys = tfRef.tensor2d(yData);
516
+ xs = tfRef.tensor2d(xData);
517
+ ys = tfRef.tensor2d(yData);
504
518
 
505
519
  const layersModel = entry.model as any;
506
520
  const result = await layersModel.fit(xs, ys, {
@@ -524,10 +538,6 @@ export class MxfMLService {
524
538
  samplesUsed: xData.length,
525
539
  };
526
540
 
527
- // Clean up tensors
528
- xs.dispose();
529
- ys.dispose();
530
-
531
541
  // Update model state
532
542
  entry.status = ModelStatus.TRAINED;
533
543
  entry.lastTrainingMetrics = metrics;
@@ -554,6 +564,9 @@ export class MxfMLService {
554
564
 
555
565
  this.logger.error(`[MxfMLService] Training failed for ${modelId}: ${errorMsg}`);
556
566
  throw error;
567
+ } finally {
568
+ xs?.dispose();
569
+ ys?.dispose();
557
570
  }
558
571
  }
559
572
 
@@ -586,6 +599,14 @@ export class MxfMLService {
586
599
  throw new Error(`[MxfMLService] Model "${modelId}" has not been built yet`);
587
600
  }
588
601
 
602
+ // Fail fast on concurrent training (same contract as train())
603
+ if (entry.status === ModelStatus.TRAINING) {
604
+ throw new Error(
605
+ `[MxfMLService] Model "${modelId}" is already training — ` +
606
+ `concurrent training on the same model is not allowed`
607
+ );
608
+ }
609
+
589
610
  // Emit training started (we don't know sample count upfront for custom training)
590
611
  this.emitTrainingStarted(modelId, 0, entry.config.epochs);
591
612
 
@@ -1067,6 +1088,11 @@ export class MxfMLService {
1067
1088
  *
1068
1089
  * Calls the provided trainCallback at the model's retrainIntervalMs.
1069
1090
  * The callback is responsible for collecting training data and calling train().
1091
+ * No-ops when auto-training is disabled (autoTrainEnabled=false).
1092
+ *
1093
+ * A model must have exactly one retraining owner: either a timer registered
1094
+ * here or a consumer-managed schedule — never both. Overlapping schedules
1095
+ * train on the same data twice and collide on TF.js fit() calls.
1070
1096
  *
1071
1097
  * @param modelId - ID of a registered model
1072
1098
  * @param trainCallback - Function to call when retraining is due
@@ -1077,6 +1103,14 @@ export class MxfMLService {
1077
1103
  ): void {
1078
1104
  const entry = this.getModelEntry(modelId);
1079
1105
 
1106
+ if (!getTensorFlowConfig().autoTrainEnabled) {
1107
+ this.logger.info(
1108
+ `[MxfMLService] Auto-training disabled (autoTrainEnabled=false), ` +
1109
+ `not scheduling retrain for ${modelId}`
1110
+ );
1111
+ return;
1112
+ }
1113
+
1080
1114
  // Clear any existing timer for this model
1081
1115
  const existingTimer = this.retrainTimers.get(modelId);
1082
1116
  if (existingTimer) {
@@ -460,13 +460,10 @@ export class PredictiveAnalyticsService extends EventEmitter {
460
460
  );
461
461
  }
462
462
 
463
- // Schedule automatic retraining with MxfMLService
464
- const config = getTensorFlowConfig();
465
- if (config.autoTrainEnabled) {
466
- mlService.scheduleRetrain(TF_ERROR_PREDICTION_MODEL_ID, async () => {
467
- await this.trainErrorPredictionModel();
468
- });
469
- }
463
+ // Retraining is owned by this service's own interval (startRetrainSchedule),
464
+ // which trains both models sequentially. Do NOT also register an
465
+ // MxfMLService.scheduleRetrain() timer — a second schedule for the same
466
+ // model trains twice per interval and collides on concurrent fit() calls.
470
467
  }
471
468
 
472
469
  // =============================================================================
@@ -563,13 +560,8 @@ export class PredictiveAnalyticsService extends EventEmitter {
563
560
  );
564
561
  }
565
562
 
566
- // Schedule automatic retraining with MxfMLService
567
- const config = getTensorFlowConfig();
568
- if (config.autoTrainEnabled) {
569
- mlService.scheduleRetrain(TF_ANOMALY_AUTOENCODER_MODEL_ID, async () => {
570
- await this.trainAnomalyDetectionModel();
571
- });
572
- }
563
+ // Retraining is owned by this service's own interval (startRetrainSchedule) —
564
+ // see the note in initializeTfErrorPrediction().
573
565
  }
574
566
 
575
567
  // =============================================================================
@@ -1739,6 +1731,8 @@ export class PredictiveAnalyticsService extends EventEmitter {
1739
1731
  *
1740
1732
  * When TF.js is enabled and sufficient training samples exist:
1741
1733
  * - Vectorizes features and binary labels from collected training data
1734
+ * - Requires both label classes to be present (see the class-balance guard
1735
+ * below) — one-class data cannot train a discriminator
1742
1736
  * - Calls MxfMLService.train() to run supervised training on the Dense classifier
1743
1737
  * - Updates internal model metadata with real training metrics
1744
1738
  * - Saves the trained model to persistent storage (GridFS)
@@ -1794,6 +1788,28 @@ export class PredictiveAnalyticsService extends EventEmitter {
1794
1788
  return;
1795
1789
  }
1796
1790
 
1791
+ // One-class data cannot train a discriminator: binary crossentropy
1792
+ // drives the sigmoid to a constant, reporting perfect accuracy while
1793
+ // learning nothing. Require both classes, each meeting a minority
1794
+ // floor that scales with the sample count.
1795
+ const positives = labels.reduce((sum, label) => sum + label, 0);
1796
+ const negatives = labels.length - positives;
1797
+ const minorityFloor = Math.max(3, Math.ceil(labels.length * 0.02));
1798
+ if (positives < minorityFloor || negatives < minorityFloor) {
1799
+ this.logger.info(
1800
+ `[PredictiveAnalyticsService] Skipping TF.js error prediction training — ` +
1801
+ `class balance too skewed (positives=${positives}, negatives=${negatives}, ` +
1802
+ `minority floor=${minorityFloor}). Heuristic prediction remains active.`
1803
+ );
1804
+ if (!this.tfErrorPredictionReady) {
1805
+ // No usable TF model — keep heuristic bookkeeping current
1806
+ this.updateHeuristicMetadata(features.length);
1807
+ }
1808
+ // A previously trained model (this session or loaded from storage)
1809
+ // keeps serving; skipping retraining must not clobber its metadata.
1810
+ return;
1811
+ }
1812
+
1797
1813
  // Format labels as 2D array for MxfMLService.train()
1798
1814
  const yData = labels.map(l => [l]);
1799
1815
 
@@ -1811,10 +1827,11 @@ export class PredictiveAnalyticsService extends EventEmitter {
1811
1827
  modelMeta.type = ModelType.NEURAL_NETWORK;
1812
1828
  modelMeta.trainedAt = Date.now();
1813
1829
  modelMeta.trainingDataSize = features.length;
1830
+ // Prefer validation accuracy — training accuracy overstates fit.
1814
1831
  // If training reported no accuracy, we do not know the accuracy. Say so.
1815
1832
  // This previously fell back to 0.75 — an invented figure indistinguishable
1816
1833
  // from a measured one.
1817
- modelMeta.accuracy = metrics.accuracy ?? metrics.valAccuracy ?? null;
1834
+ modelMeta.accuracy = metrics.valAccuracy ?? metrics.accuracy ?? null;
1818
1835
  modelMeta.validationMetrics = {
1819
1836
  accuracy: metrics.accuracy ?? 0,
1820
1837
  val_accuracy: metrics.valAccuracy ?? 0,
@@ -2254,13 +2271,23 @@ export class PredictiveAnalyticsService extends EventEmitter {
2254
2271
  /**
2255
2272
  * Start retrain schedule.
2256
2273
  *
2257
- * When TF.js is enabled, both error prediction and anomaly detection
2258
- * retraining are managed by MxfMLService.scheduleRetrain() (configured
2259
- * in initializeTfErrorPrediction() and initializeTfAnomalyAutoencoder()).
2260
- * This interval serves as the retraining path when TF.js is disabled
2261
- * and as a fallback when MxfMLService scheduling is not active.
2274
+ * This interval is the single owner of scheduled retraining. trainModels()
2275
+ * trains the error predictor and the anomaly autoencoder sequentially —
2276
+ * TF.js rejects overlapping fit() calls on one model, and a single timer
2277
+ * avoids overlapping training cycles entirely. Registering per-model
2278
+ * MxfMLService.scheduleRetrain() timers alongside this interval caused
2279
+ * duplicate hourly training and fit() collisions.
2280
+ *
2281
+ * When TF.js is enabled, autoTrainEnabled gates scheduled retraining.
2282
+ * When TF.js is disabled, the interval refreshes heuristic metadata.
2262
2283
  */
2263
2284
  private startRetrainSchedule(): void {
2285
+ if (isTensorFlowEnabled() && !getTensorFlowConfig().autoTrainEnabled) {
2286
+ this.logger.info(
2287
+ '[PredictiveAnalyticsService] TF.js auto-training disabled, not scheduling model retraining'
2288
+ );
2289
+ return;
2290
+ }
2264
2291
  this.retrainInterval = setInterval(() => {
2265
2292
  this.trainModels();
2266
2293
  }, this.config.retrainInterval);
@@ -34,6 +34,13 @@ export enum NetworkErrorType {
34
34
  NETWORK_CONNECTION_REFUSED = 'NETWORK_CONNECTION_REFUSED',
35
35
  NETWORK_DNS_RESOLUTION = 'NETWORK_DNS_RESOLUTION',
36
36
  NETWORK_SOCKET_ERROR = 'NETWORK_SOCKET_ERROR',
37
+
38
+ // Hard per-request timeout: the request ran for the full configured bound
39
+ // (requestTimeoutMs / a stream idle bound) without completing. Deliberately
40
+ // NOT retryable — retrying a request that just consumed the entire timeout
41
+ // budget would multiply the silence the timeout exists to end. The caller
42
+ // gets the failure immediately.
43
+ REQUEST_TIMEOUT = 'REQUEST_TIMEOUT',
37
44
 
38
45
  // API service issues
39
46
  API_BAD_GATEWAY = 'API_BAD_GATEWAY', // 502
@@ -205,9 +212,18 @@ export function classifyNetworkError(
205
212
  }
206
213
  }
207
214
 
215
+ // Hard request timeouts are marked explicitly: either by AbortSignal.timeout()
216
+ // (DOMException named 'TimeoutError' in both Node and Bun) or by the
217
+ // isRequestTimeout flag set where the timeout is enforced. Checked before the
218
+ // message heuristics below so a hard timeout is never misclassified as a
219
+ // retryable NETWORK_TIMEOUT.
220
+ if (error?.isRequestTimeout === true || error?.name === 'TimeoutError') {
221
+ return NetworkErrorType.REQUEST_TIMEOUT;
222
+ }
223
+
208
224
  // Check error message for network issues
209
225
  const errorMessage = error?.message?.toLowerCase() || '';
210
-
226
+
211
227
  if (errorMessage.includes('timeout')) {
212
228
  return NetworkErrorType.NETWORK_TIMEOUT;
213
229
  }