@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.
- package/dist/protocols/mcp/providers/OpenRouterMcpClient.d.ts +34 -4
- package/dist/protocols/mcp/providers/OpenRouterMcpClient.d.ts.map +1 -1
- package/dist/protocols/mcp/providers/OpenRouterMcpClient.js +297 -65
- package/dist/protocols/mcp/providers/OpenRouterMcpClient.js.map +1 -1
- package/dist/protocols/mcp/utils/NetworkRecovery.d.ts +21 -0
- package/dist/protocols/mcp/utils/NetworkRecovery.d.ts.map +1 -1
- package/dist/protocols/mcp/utils/NetworkRecovery.js +62 -2
- package/dist/protocols/mcp/utils/NetworkRecovery.js.map +1 -1
- package/dist/services/MemoryService.d.ts +17 -1
- package/dist/services/MemoryService.d.ts.map +1 -1
- package/dist/services/MemoryService.js +33 -13
- package/dist/services/MemoryService.js.map +1 -1
- package/dist/services/MxfMLService.d.ts +5 -0
- package/dist/services/MxfMLService.d.ts.map +1 -1
- package/dist/services/MxfMLService.js +32 -6
- package/dist/services/MxfMLService.js.map +1 -1
- package/dist/services/PredictiveAnalyticsService.d.ts +11 -5
- package/dist/services/PredictiveAnalyticsService.d.ts.map +1 -1
- package/dist/services/PredictiveAnalyticsService.js +42 -20
- package/dist/services/PredictiveAnalyticsService.js.map +1 -1
- package/dist/types/NetworkRecoveryTypes.d.ts +1 -0
- package/dist/types/NetworkRecoveryTypes.d.ts.map +1 -1
- package/dist/types/NetworkRecoveryTypes.js +14 -0
- package/dist/types/NetworkRecoveryTypes.js.map +1 -1
- package/package.json +1 -1
- package/src/protocols/mcp/providers/OpenRouterMcpClient.ts +351 -83
- package/src/protocols/mcp/utils/NetworkRecovery.ts +74 -3
- package/src/services/MemoryService.ts +40 -16
- package/src/services/MxfMLService.ts +41 -7
- package/src/services/PredictiveAnalyticsService.ts +47 -20
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
790
|
-
const
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
819
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
//
|
|
567
|
-
|
|
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.
|
|
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
|
-
*
|
|
2258
|
-
*
|
|
2259
|
-
*
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
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
|
}
|