vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
503 lines (502 loc) • 20.8 kB
JavaScript
import { jobManager, JobStatus } from '../../../services/job-manager/index.js';
import { getTimeoutManager } from '../utils/timeout-manager.js';
import logger from '../../../logger.js';
import { EventEmitter } from 'events';
export class JobManagerIntegrationService extends EventEmitter {
static instance;
taskJobs = new Map();
jobMetrics = new Map();
jobQueue = [];
runningJobs = new Set();
jobSubscriptions = new Map();
progressSubscriptions = new Map();
config = null;
userConfig;
processingInterval;
initialized = false;
constructor(config) {
super();
this.userConfig = config;
logger.info('Job Manager Integration Service initialized (job processor deferred)');
}
getConfig() {
if (!this.config) {
const timeoutManager = getTimeoutManager();
this.config = {
maxConcurrentJobs: 5,
priorityWeights: {
'critical': 4,
'high': 3,
'medium': 2,
'low': 1
},
retryPolicy: {
maxRetries: 3,
backoffMultiplier: 2,
initialDelayMs: 1000
},
timeoutPolicy: {
defaultTimeoutMs: timeoutManager.getTimeout('taskExecution'),
operationTimeouts: {
'decomposition': timeoutManager.getTimeout('taskDecomposition'),
'execution': timeoutManager.getTimeout('taskExecution'),
'validation': timeoutManager.getTimeout('databaseOperations'),
'analysis': timeoutManager.getTimeout('taskRefinement'),
'codemap': timeoutManager.getTimeout('fileOperations'),
'context_enrichment': timeoutManager.getTimeout('taskRefinement')
}
},
resourceLimits: {
maxMemoryMB: 2048,
maxCpuWeight: 8,
maxDiskSpaceMB: 1024
},
...this.userConfig
};
logger.debug('Job Manager configuration initialized with timeout values');
this.ensureInitialized();
}
return this.config;
}
ensureInitialized() {
if (!this.initialized) {
this.initialized = true;
this.startJobProcessor();
logger.debug('Job Manager Integration Service job processor started');
}
}
static getInstance(config) {
if (!JobManagerIntegrationService.instance) {
JobManagerIntegrationService.instance = new JobManagerIntegrationService(config);
}
return JobManagerIntegrationService.instance;
}
async createTaskJob(toolName, params, options) {
try {
const jobId = jobManager.createJob(toolName, params);
const taskJob = {
...jobManager.getJob(jobId),
taskId: options.taskId,
projectId: options.projectId,
operationType: options.operationType,
priority: options.priority || 'medium',
estimatedDuration: options.estimatedDuration,
resourceRequirements: options.resourceRequirements || {
memoryMB: 256,
cpuWeight: 1
},
dependencies: options.dependencies || [],
metadata: {
retryCount: 0,
maxRetries: this.getConfig().retryPolicy.maxRetries,
...options.metadata
}
};
this.taskJobs.set(jobId, taskJob);
this.jobMetrics.set(jobId, {
jobId,
startTime: Date.now(),
resourceUsage: {
peakMemoryMB: 0,
averageCpuUsage: 0
},
performanceScore: 0,
errorCount: 0,
retryCount: 0
});
if (taskJob.dependencies && taskJob.dependencies.length > 0) {
this.jobQueue.push(taskJob);
logger.info({ jobId, dependencies: taskJob.dependencies }, 'Job queued due to dependencies');
}
else {
this.jobQueue.push(taskJob);
logger.info({ jobId, operationType: taskJob.operationType }, 'Job queued for execution');
}
this.emit('job_created', taskJob);
return jobId;
}
catch (error) {
logger.error({ err: error, toolName, options }, 'Failed to create task job');
throw new Error(`Failed to create task job: ${error instanceof Error ? error.message : String(error)}`);
}
}
getTaskJob(jobId) {
return this.taskJobs.get(jobId) || null;
}
getJobMetrics(jobId) {
return this.jobMetrics.get(jobId) || null;
}
async updateJobProgress(jobId, progress, message, resourceUsage) {
try {
const updated = jobManager.updateJobStatus(jobId, JobStatus.RUNNING, message, progress);
if (!updated) {
return false;
}
const metrics = this.jobMetrics.get(jobId);
if (metrics && resourceUsage) {
metrics.resourceUsage = {
...metrics.resourceUsage,
...resourceUsage
};
}
const progressCallbacks = this.progressSubscriptions.get(jobId) || [];
progressCallbacks.forEach(callback => {
try {
callback(jobId, progress, message);
}
catch (error) {
logger.error({ err: error, jobId }, 'Error in progress callback');
}
});
this.emit('job_progress', jobId, progress, message);
return true;
}
catch (error) {
logger.error({ err: error, jobId }, 'Failed to update job progress');
return false;
}
}
async completeJob(jobId, result, finalMetrics) {
try {
const success = jobManager.setJobResult(jobId, {
isError: false,
content: [{ type: 'text', text: JSON.stringify(result) }]
});
if (!success) {
return false;
}
const metrics = this.jobMetrics.get(jobId);
if (metrics) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
if (finalMetrics) {
Object.assign(metrics, finalMetrics);
}
metrics.performanceScore = this.calculatePerformanceScore(metrics);
}
this.runningJobs.delete(jobId);
const taskJob = this.taskJobs.get(jobId);
if (taskJob) {
const callbacks = this.jobSubscriptions.get(jobId) || [];
callbacks.forEach(callback => {
try {
callback(taskJob, metrics);
}
catch (error) {
logger.error({ err: error, jobId }, 'Error in job completion callback');
}
});
}
this.emit('job_completed', jobId, result, metrics);
logger.info({ jobId, duration: metrics?.duration }, 'Job completed successfully');
return true;
}
catch (error) {
logger.error({ err: error, jobId }, 'Failed to complete job');
return false;
}
}
async failJob(jobId, error, shouldRetry = true) {
try {
const taskJob = this.taskJobs.get(jobId);
const metrics = this.jobMetrics.get(jobId);
if (metrics) {
metrics.errorCount++;
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
}
if (shouldRetry && taskJob && metrics) {
const maxRetries = taskJob.metadata?.maxRetries || this.getConfig().retryPolicy.maxRetries;
const currentRetries = metrics.retryCount;
if (currentRetries < maxRetries) {
metrics.retryCount++;
const delay = this.getConfig().retryPolicy.initialDelayMs *
Math.pow(this.getConfig().retryPolicy.backoffMultiplier, currentRetries);
logger.info({ jobId, retryCount: metrics.retryCount, delay }, 'Scheduling job retry');
setTimeout(() => {
this.jobQueue.unshift(taskJob);
}, delay);
return true;
}
}
const success = jobManager.setJobResult(jobId, {
isError: true,
content: [{ type: 'text', text: error.message }]
});
this.runningJobs.delete(jobId);
this.emit('job_failed', jobId, error, metrics);
logger.error({ err: error, jobId, retryCount: metrics?.retryCount }, 'Job failed permanently');
return success;
}
catch (err) {
logger.error({ err, jobId }, 'Failed to handle job failure');
return false;
}
}
async cancelJob(jobId, reason) {
try {
const taskJob = this.taskJobs.get(jobId);
if (!taskJob) {
logger.warn({ jobId }, 'Attempted to cancel non-existent task job');
return false;
}
const success = jobManager.setJobResult(jobId, {
isError: true,
content: [{ type: 'text', text: reason || 'Job cancelled by user' }]
});
if (success) {
this.runningJobs.delete(jobId);
const queueIndex = this.jobQueue.findIndex(job => job.id === jobId);
if (queueIndex !== -1) {
this.jobQueue.splice(queueIndex, 1);
}
const metrics = this.jobMetrics.get(jobId);
if (metrics) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
}
this.emit('job_cancelled', jobId, reason, metrics);
logger.info({ jobId, reason }, 'Job cancelled successfully');
}
return success;
}
catch (error) {
logger.error({ err: error, jobId }, 'Failed to cancel job');
return false;
}
}
getQueueStatus() {
const jobsByPriority = {};
const jobsByOperation = {};
this.jobQueue.forEach(job => {
jobsByPriority[job.priority] = (jobsByPriority[job.priority] || 0) + 1;
jobsByOperation[job.operationType] = (jobsByOperation[job.operationType] || 0) + 1;
});
const now = Date.now();
const waitTimes = this.jobQueue.map(job => now - job.createdAt);
const averageWaitTime = waitTimes.length > 0
? waitTimes.reduce((sum, time) => sum + time, 0) / waitTimes.length
: 0;
return {
queueLength: this.jobQueue.length,
runningJobs: this.runningJobs.size,
totalJobs: this.taskJobs.size,
jobsByPriority,
jobsByOperation,
averageWaitTime
};
}
getJobStatistics() {
const allJobs = Array.from(this.taskJobs.values());
const allMetrics = Array.from(this.jobMetrics.values());
const completedJobs = allJobs.filter(job => job.status === JobStatus.COMPLETED).length;
const failedJobs = allJobs.filter(job => job.status === JobStatus.FAILED).length;
const runningJobs = allJobs.filter(job => job.status === JobStatus.RUNNING).length;
const queuedJobs = allJobs.filter(job => job.status === JobStatus.PENDING).length;
const completedMetrics = allMetrics.filter(m => m.duration !== undefined);
const averageExecutionTime = completedMetrics.length > 0
? completedMetrics.reduce((sum, m) => sum + (m.duration || 0), 0) / completedMetrics.length
: 0;
const averagePerformanceScore = allMetrics.length > 0
? allMetrics.reduce((sum, m) => sum + m.performanceScore, 0) / allMetrics.length
: 0;
const averageMemoryMB = allMetrics.length > 0
? allMetrics.reduce((sum, m) => sum + m.resourceUsage.peakMemoryMB, 0) / allMetrics.length
: 0;
const averageCpuUsage = allMetrics.length > 0
? allMetrics.reduce((sum, m) => sum + m.resourceUsage.averageCpuUsage, 0) / allMetrics.length
: 0;
const peakMemoryMB = Math.max(...allMetrics.map(m => m.resourceUsage.peakMemoryMB), 0);
const operationStats = {};
allJobs.forEach(job => {
const opType = job.operationType;
if (!operationStats[opType]) {
operationStats[opType] = { count: 0, averageTime: 0, successRate: 0 };
}
operationStats[opType].count++;
});
Object.keys(operationStats).forEach(opType => {
const jobsOfType = allJobs.filter(job => job.operationType === opType);
const metricsOfType = jobsOfType
.map(job => this.jobMetrics.get(job.id))
.filter(m => m && m.duration !== undefined);
const completedOfType = jobsOfType.filter(job => job.status === JobStatus.COMPLETED).length;
const totalOfType = jobsOfType.length;
operationStats[opType].averageTime = metricsOfType.length > 0
? metricsOfType.reduce((sum, m) => sum + (m.duration || 0), 0) / metricsOfType.length
: 0;
operationStats[opType].successRate = totalOfType > 0 ? completedOfType / totalOfType : 0;
});
return {
totalJobs: allJobs.length,
completedJobs,
failedJobs,
runningJobs,
queuedJobs,
averageExecutionTime,
averagePerformanceScore,
resourceUtilization: {
averageMemoryMB,
averageCpuUsage,
peakMemoryMB
},
operationStats
};
}
subscribeToJob(jobId, callback) {
if (!this.jobSubscriptions.has(jobId)) {
this.jobSubscriptions.set(jobId, []);
}
this.jobSubscriptions.get(jobId).push(callback);
return () => {
const callbacks = this.jobSubscriptions.get(jobId);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
};
}
subscribeToJobProgress(jobId, callback) {
if (!this.progressSubscriptions.has(jobId)) {
this.progressSubscriptions.set(jobId, []);
}
this.progressSubscriptions.get(jobId).push(callback);
return () => {
const callbacks = this.progressSubscriptions.get(jobId);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
};
}
updateConfig(newConfig) {
const currentConfig = this.getConfig();
this.config = { ...currentConfig, ...newConfig };
logger.info({ config: this.config }, 'Job manager configuration updated');
this.emit('config_updated', this.config);
}
cleanupOldJobs(maxAgeMs = 24 * 60 * 60 * 1000) {
const now = Date.now();
let cleanedCount = 0;
for (const [jobId, job] of this.taskJobs.entries()) {
if ((job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) &&
(now - job.updatedAt) > maxAgeMs) {
this.taskJobs.delete(jobId);
this.jobMetrics.delete(jobId);
this.jobSubscriptions.delete(jobId);
this.progressSubscriptions.delete(jobId);
cleanedCount++;
}
}
if (cleanedCount > 0) {
logger.info({ cleanedCount, maxAgeMs }, 'Cleaned up old jobs');
this.emit('jobs_cleaned', cleanedCount);
}
return cleanedCount;
}
dispose() {
if (this.processingInterval) {
clearInterval(this.processingInterval);
this.processingInterval = undefined;
}
this.taskJobs.clear();
this.jobMetrics.clear();
this.jobQueue.length = 0;
this.runningJobs.clear();
this.jobSubscriptions.clear();
this.progressSubscriptions.clear();
this.removeAllListeners();
logger.info('Job Manager Integration Service disposed');
}
startJobProcessor() {
this.processingInterval = setInterval(() => {
this.processJobQueue().catch(error => {
logger.error({ err: error }, 'Error in job processing loop');
});
}, 1000);
logger.debug('Job processor started');
}
async processJobQueue() {
if (this.jobQueue.length === 0 || this.runningJobs.size >= this.getConfig().maxConcurrentJobs) {
return;
}
this.jobQueue.sort((a, b) => {
const priorityDiff = this.getConfig().priorityWeights[b.priority] - this.getConfig().priorityWeights[a.priority];
if (priorityDiff !== 0)
return priorityDiff;
return a.createdAt - b.createdAt;
});
const readyJobs = this.jobQueue.filter(job => this.areDependenciesSatisfied(job));
const jobsToStart = readyJobs.slice(0, this.getConfig().maxConcurrentJobs - this.runningJobs.size);
for (const job of jobsToStart) {
await this.startJob(job);
}
}
areDependenciesSatisfied(job) {
if (!job.dependencies || job.dependencies.length === 0) {
return true;
}
return job.dependencies.every(depJobId => {
const depJob = this.taskJobs.get(depJobId);
return depJob && depJob.status === JobStatus.COMPLETED;
});
}
async startJob(job) {
try {
const queueIndex = this.jobQueue.findIndex(queuedJob => queuedJob.id === job.id);
if (queueIndex !== -1) {
this.jobQueue.splice(queueIndex, 1);
}
this.runningJobs.add(job.id);
jobManager.updateJobStatus(job.id, JobStatus.RUNNING, 'Job started');
const metrics = this.jobMetrics.get(job.id);
if (metrics) {
metrics.startTime = Date.now();
}
const timeoutMs = this.getConfig().timeoutPolicy.operationTimeouts[job.operationType] ||
this.getConfig().timeoutPolicy.defaultTimeoutMs;
setTimeout(() => {
if (this.runningJobs.has(job.id)) {
this.handleJobTimeout(job.id);
}
}, timeoutMs);
this.emit('job_started', job);
logger.info({ jobId: job.id, operationType: job.operationType }, 'Job started');
}
catch (error) {
logger.error({ err: error, jobId: job.id }, 'Failed to start job');
await this.failJob(job.id, error instanceof Error ? error : new Error(String(error)), false);
}
}
async handleJobTimeout(jobId) {
const job = this.taskJobs.get(jobId);
if (!job)
return;
logger.warn({ jobId, operationType: job.operationType }, 'Job timed out');
const metrics = this.jobMetrics.get(jobId);
if (metrics) {
metrics.errorCount++;
}
await this.failJob(jobId, new Error('Job execution timed out'), true);
}
calculatePerformanceScore(metrics) {
let score = 100;
score -= metrics.errorCount * 10;
score -= metrics.retryCount * 5;
if (metrics.resourceUsage.peakMemoryMB < 512) {
score += 5;
}
if (metrics.resourceUsage.averageCpuUsage < 50) {
score += 5;
}
return Math.max(0, Math.min(100, score));
}
}
export const jobManagerIntegration = JobManagerIntegrationService.getInstance();
export function getJobManagerIntegration(config) {
return JobManagerIntegrationService.getInstance(config);
}