fraim 2.0.340 → 2.0.341
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/registry/scripts/learning/completion-state.js +10 -2
- package/dist/src/core/handoff-contracts.js +122 -21
- package/dist/src/core/proposal-set-contract.js +214 -0
- package/dist/src/core/run-recovery-state.js +9 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +128 -1
- package/dist/src/local-mcp-server/learning-usage-analysis.js +35 -19
- package/dist/src/local-mcp-server/learning-usage-command.js +8 -4
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +59 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -1
- package/dist/src/local-mcp-server/learning-usage-store.js +163 -38
- package/dist/src/local-mcp-server/learning-usage-verify.js +3 -1
- package/dist/src/local-mcp-server/stdio-server.js +266 -112
- package/dist/src/local-mcp-server/usage-collector.js +5 -29
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/routes/analytics.js +10 -3
- package/dist/src/services/mcp-service.js +71 -11
- package/dist/src/services/usage-analytics-service.js +6 -3
- package/package.json +1 -1
|
@@ -745,7 +745,11 @@ router.post('/events', async (req, res) => {
|
|
|
745
745
|
if (!analyticsService) {
|
|
746
746
|
throw new Error('Analytics service not initialized');
|
|
747
747
|
}
|
|
748
|
-
|
|
748
|
+
// Wait for every write before cleanup, even if another write fails.
|
|
749
|
+
const results = await Promise.allSettled(processedEvents.map(event => analyticsService.logUsageImmediate(event, { throwOnError: true })));
|
|
750
|
+
const failure = results.find(result => result.status === 'rejected');
|
|
751
|
+
if (failure?.status === 'rejected')
|
|
752
|
+
throw failure.reason;
|
|
749
753
|
res.json({
|
|
750
754
|
message: 'Events processed',
|
|
751
755
|
count: processedEvents.length
|
|
@@ -756,8 +760,11 @@ router.post('/events', async (req, res) => {
|
|
|
756
760
|
res.status(500).json({ error: 'Internal server error' });
|
|
757
761
|
}
|
|
758
762
|
finally {
|
|
759
|
-
|
|
760
|
-
await
|
|
763
|
+
try {
|
|
764
|
+
await analyticsService?.shutdown();
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
await dbService?.close();
|
|
761
768
|
}
|
|
762
769
|
}
|
|
763
770
|
});
|
|
@@ -46,6 +46,7 @@ const compat_1 = require("../config/compat");
|
|
|
46
46
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
47
47
|
const quality_evidence_1 = require("../core/quality-evidence");
|
|
48
48
|
const handoff_contracts_1 = require("../core/handoff-contracts");
|
|
49
|
+
const test_evidence_contract_1 = require("../core/test-evidence-contract");
|
|
49
50
|
const feature_flags_1 = require("../config/feature-flags");
|
|
50
51
|
const persona_entitlement_service_1 = require("./persona-entitlement-service");
|
|
51
52
|
exports.DEFAULT_LAUNCH_PHRASE_MAPPINGS = {
|
|
@@ -69,6 +70,7 @@ function buildTrainingModeBlock(mode) {
|
|
|
69
70
|
}
|
|
70
71
|
class McpService {
|
|
71
72
|
constructor(registryService, sessionManager, aiMentor, dbService, analyticsServiceOrVersion, serverVersion) {
|
|
73
|
+
this.phaseStartMarkers = new Map();
|
|
72
74
|
this.registryService = registryService;
|
|
73
75
|
this.sessionManager = sessionManager;
|
|
74
76
|
this.aiMentor = aiMentor;
|
|
@@ -226,8 +228,8 @@ class McpService {
|
|
|
226
228
|
throw new Error('Argument "job" is required for get_fraim_job');
|
|
227
229
|
return await this.handleGetJob(args.job, context.apiKey, context.userId);
|
|
228
230
|
case 'seekMentoring':
|
|
229
|
-
if (!args.jobName || !args.issueNumber || !args.currentPhase || !args.status
|
|
230
|
-
throw new Error('Arguments "jobName", "
|
|
231
|
+
if (!args.jobName || !args.issueNumber || !args.currentPhase || !args.status) {
|
|
232
|
+
throw new Error('Arguments "jobName", "issueNumber", "currentPhase", and "status" are required for seekMentoring');
|
|
231
233
|
}
|
|
232
234
|
return await this.handleSeekMentoring(args, context.userId);
|
|
233
235
|
case 'list_fraim_jobs':
|
|
@@ -692,10 +694,15 @@ class McpService {
|
|
|
692
694
|
if (personaLock) {
|
|
693
695
|
return personaLock;
|
|
694
696
|
}
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
+
// Missing IDs still allow mentoring and session-scoped start matching.
|
|
698
|
+
// Unknown IDs must not produce analytics records for another run.
|
|
699
|
+
let trackingWarning = '';
|
|
700
|
+
let canTrackJob = Boolean(args.jobId && userId && this.analyticsService);
|
|
701
|
+
if (canTrackJob) {
|
|
697
702
|
const isValidJob = await this.analyticsService.validateJobId(args.jobId, userId);
|
|
698
703
|
if (!isValidJob) {
|
|
704
|
+
canTrackJob = false;
|
|
705
|
+
trackingWarning = `Job tracking notice: jobId \`${args.jobId}\` is unrecognized for this user; continuing without job analytics.\n\n`;
|
|
699
706
|
console.warn(`[seekMentoring] Unrecognized jobId: ${args.jobId} for user ${userId} — proceeding without tracking`);
|
|
700
707
|
}
|
|
701
708
|
}
|
|
@@ -744,10 +751,21 @@ class McpService {
|
|
|
744
751
|
: 'enforce';
|
|
745
752
|
if (reviewHandoffApplies && enforcementMode === 'enforce') {
|
|
746
753
|
return {
|
|
747
|
-
content: [{ type: 'text', text: (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors) }]
|
|
754
|
+
content: [{ type: 'text', text: trackingWarning + (0, handoff_contracts_1.buildHandoffRejectionMessage)(args.currentPhase, missingField, handoffErrors) }]
|
|
748
755
|
};
|
|
749
756
|
}
|
|
750
757
|
}
|
|
758
|
+
const testEvidenceErrors = (0, test_evidence_contract_1.validateTestEvidenceContract)({
|
|
759
|
+
currentPhase: args.currentPhase,
|
|
760
|
+
status: args.status,
|
|
761
|
+
findings: args.findings,
|
|
762
|
+
phases: handoffPhaseMap,
|
|
763
|
+
});
|
|
764
|
+
if (testEvidenceErrors.length > 0) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: 'text', text: trackingWarning + (0, test_evidence_contract_1.buildTestEvidenceRejectionMessage)(args.currentPhase, testEvidenceErrors) }]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
751
769
|
// Quality enforcement (Issue #251): quality-producing jobs MUST emit
|
|
752
770
|
// a valid `evidence.quality` object on their final completion call.
|
|
753
771
|
//
|
|
@@ -771,7 +789,7 @@ class McpService {
|
|
|
771
789
|
];
|
|
772
790
|
if (qualityErrors.length > 0) {
|
|
773
791
|
const rejectionMessage = this.buildQualityRejectionMessage(args.jobName, args.currentPhase, qualityErrors);
|
|
774
|
-
if (userId && this.analyticsService) {
|
|
792
|
+
if (canTrackJob && userId && this.analyticsService) {
|
|
775
793
|
try {
|
|
776
794
|
const sessionId = args.sessionId || 'unknown';
|
|
777
795
|
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, 'incomplete', args.currentPhase, false);
|
|
@@ -781,17 +799,58 @@ class McpService {
|
|
|
781
799
|
}
|
|
782
800
|
}
|
|
783
801
|
return {
|
|
784
|
-
content: [{ type: 'text', text: rejectionMessage }]
|
|
802
|
+
content: [{ type: 'text', text: trackingWarning + rejectionMessage }]
|
|
785
803
|
};
|
|
786
804
|
}
|
|
787
805
|
}
|
|
806
|
+
// Only accepted calls update the marker. A rejected completion must
|
|
807
|
+
// leave its start available for the corrected completion request.
|
|
808
|
+
let phaseStartWarning = '';
|
|
809
|
+
const sessionId = typeof args.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
810
|
+
const isPhaseCompletion = args.status === 'complete' || args.status === 'failure';
|
|
811
|
+
if (sessionId && sessionId !== 'unknown') {
|
|
812
|
+
const markerKey = JSON.stringify([userId ?? null, sessionId, args.jobName]);
|
|
813
|
+
const context = {
|
|
814
|
+
sessionId,
|
|
815
|
+
jobName: args.jobName,
|
|
816
|
+
phaseId: args.currentPhase,
|
|
817
|
+
...(args.jobId ? { jobId: args.jobId } : {}),
|
|
818
|
+
};
|
|
819
|
+
const marker = this.phaseStartMarkers.get(markerKey);
|
|
820
|
+
const timestamp = new Date().toISOString();
|
|
821
|
+
if (args.status === 'starting') {
|
|
822
|
+
this.phaseStartMarkers.set(markerKey, (0, handoff_contracts_1.recordPhaseStart)(marker, { ...context, timestamp }));
|
|
823
|
+
}
|
|
824
|
+
else if (isPhaseCompletion) {
|
|
825
|
+
const inspection = (0, handoff_contracts_1.inspectPhaseCompletion)(marker, context);
|
|
826
|
+
if (inspection.missingStart)
|
|
827
|
+
phaseStartWarning = (0, handoff_contracts_1.buildPhaseStartWarning)(args.currentPhase);
|
|
828
|
+
const completed = (0, handoff_contracts_1.commitPhaseCompletion)(marker, inspection, { ...context, timestamp });
|
|
829
|
+
this.phaseStartMarkers.set(markerKey, completed);
|
|
830
|
+
if (args.status === 'complete' && result.nextPhase === null) {
|
|
831
|
+
// Concurrent runs can share the session and job name.
|
|
832
|
+
// Clear only this run (including starts before its ID was known).
|
|
833
|
+
const completedJobId = args.jobId || completed.phaseInstances?.find((instance) => instance.instanceId === inspection.instanceId)?.jobId;
|
|
834
|
+
const remaining = completed.phaseInstances?.filter((instance) => instance.jobId && instance.jobId !== completedJobId) || [];
|
|
835
|
+
if (remaining.length > 0) {
|
|
836
|
+
this.phaseStartMarkers.set(markerKey, { phaseInstances: remaining });
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
this.phaseStartMarkers.delete(markerKey);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (isPhaseCompletion) {
|
|
845
|
+
phaseStartWarning = `Phase-start notice: phase \`${args.currentPhase}\` has no identifiable session, so its start cannot be matched to this run. The missing match was flagged, but this completion remains accepted.\n\n`;
|
|
846
|
+
}
|
|
788
847
|
// Update job execution tracking using usage events
|
|
789
|
-
if (userId && this.analyticsService) {
|
|
848
|
+
if (canTrackJob && userId && this.analyticsService) {
|
|
790
849
|
try {
|
|
791
850
|
const sessionId = args.sessionId || 'unknown';
|
|
792
851
|
await this.analyticsService.logMentoringCall(args.jobId, args.jobName, userId, sessionId, args.currentPhase, args.status, result.nextPhase, result.message ? true : false);
|
|
793
852
|
// Log explicit completion event when job finishes
|
|
794
|
-
if (
|
|
853
|
+
if (args.status === 'complete' && result.nextPhase === null) {
|
|
795
854
|
await this.analyticsService.logJobComplete(args.jobId, args.jobName, userId, sessionId, args.currentPhase);
|
|
796
855
|
// Emit quality score for jobs that produce quality assessments (Issue #251).
|
|
797
856
|
// Validation already happened above for quality-producing jobs, so this
|
|
@@ -818,8 +877,9 @@ class McpService {
|
|
|
818
877
|
return {
|
|
819
878
|
content: [{
|
|
820
879
|
type: 'text',
|
|
821
|
-
text: result.message
|
|
822
|
-
}]
|
|
880
|
+
text: trackingWarning + phaseStartWarning + result.message
|
|
881
|
+
}],
|
|
882
|
+
structuredContent: { phaseAccepted: true, nextPhase: result.nextPhase, phaseStartWarning }
|
|
823
883
|
};
|
|
824
884
|
}
|
|
825
885
|
getJobDescription(jobName, fullPath) {
|
|
@@ -63,9 +63,10 @@ class UsageAnalyticsService {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Log a usage event immediately (bypasses queue)
|
|
66
|
+
* Log a usage event immediately (bypasses queue).
|
|
67
|
+
* Ingestion callers can require persistence; other callers remain best effort.
|
|
67
68
|
*/
|
|
68
|
-
async logUsageImmediate(input) {
|
|
69
|
+
async logUsageImmediate(input, options = {}) {
|
|
69
70
|
try {
|
|
70
71
|
// Enrich with category if missing and type is job
|
|
71
72
|
if (input.type === 'job' && (!input.category || input.category === 'uncategorized')) {
|
|
@@ -75,7 +76,9 @@ class UsageAnalyticsService {
|
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
78
|
console.error('Failed to log usage event:', error);
|
|
78
|
-
|
|
79
|
+
if (options.throwOnError)
|
|
80
|
+
throw error;
|
|
81
|
+
// Analytics failures should not break unrelated main functionality.
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
/**
|