context-forge
Version:
AI orchestration platform with autonomous teams, enhancement planning, migration tools, 25+ slash commands, checkpoints & hooks. Multi-IDE: Claude, Cursor, Windsurf, Cline, Copilot
278 lines (270 loc) • 10.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelfSchedulingService = void 0;
const events_1 = require("events");
const chalk_1 = __importDefault(require("chalk"));
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const uuid_1 = require("uuid");
class SelfSchedulingService extends events_1.EventEmitter {
constructor(config, tmux) {
super();
this.schedules = new Map();
this.stats = {
totalScheduled: 0,
executed: 0,
failed: 0,
cancelled: 0,
averageInterval: 0,
adaptiveAdjustments: 0,
};
this.workloadMetrics = new Map(); // agentId -> workload score
this.config = config;
this.tmux = tmux;
}
/**
* Schedule next check-in for an agent
*/
async scheduleAgentCheckIn(agentSession, interval, note) {
// Determine interval
let checkInterval = interval || this.config.defaultCheckInterval;
// Apply adaptive scheduling if enabled
if (this.config.adaptiveScheduling) {
checkInterval = this.calculateAdaptiveInterval(agentSession.agentId);
}
// Ensure within bounds
checkInterval = Math.max(this.config.minCheckInterval, Math.min(this.config.maxCheckInterval, checkInterval));
const scheduleEntry = {
id: (0, uuid_1.v4)(),
agentId: agentSession.agentId,
sessionName: agentSession.sessionName,
windowIndex: agentSession.windowIndex,
scheduledTime: new Date(Date.now() + checkInterval * 60 * 1000),
interval: checkInterval,
note: note || `Regular check-in for ${agentSession.agentId}`,
status: 'pending',
};
// Execute scheduling command
const processId = await this.executeSchedule(scheduleEntry);
if (processId) {
scheduleEntry.processId = processId;
}
// Track schedule
this.schedules.set(agentSession.agentId, scheduleEntry);
this.stats.totalScheduled++;
this.updateAverageInterval();
// Emit event
this.emit('scheduled', scheduleEntry);
return scheduleEntry;
}
/**
* Calculate adaptive interval based on agent workload
*/
calculateAdaptiveInterval(agentId) {
const workload = this.workloadMetrics.get(agentId) || 50; // 0-100 scale
// High workload = shorter intervals, low workload = longer intervals
const range = this.config.maxCheckInterval - this.config.minCheckInterval;
const adaptedInterval = this.config.maxCheckInterval - (workload / 100) * range;
this.stats.adaptiveAdjustments++;
console.log(chalk_1.default.gray(`Adaptive scheduling for ${agentId}: workload=${workload}, interval=${Math.round(adaptedInterval)}min`));
return Math.round(adaptedInterval);
}
/**
* Update agent workload metrics
*/
updateWorkloadMetrics(agentId, metrics) {
// Calculate workload score (0-100)
const workload = Math.min(100, metrics.tasksPending * 10 +
metrics.blockers * 20 +
metrics.messagesReceived * 2 -
metrics.tasksCompleted * 5);
this.workloadMetrics.set(agentId, Math.max(0, workload));
}
/**
* Execute schedule using scheduling script
*/
async executeSchedule(schedule) {
try {
// Create schedule command
const scheduleScript = this.config.scheduleScript ||
path_1.default.join(process.cwd(), '.claude', 'orchestration', 'schedule.sh');
if (!(await fs_extra_1.default.pathExists(scheduleScript))) {
throw new Error(`Schedule script not found: ${scheduleScript}`);
}
// Build command
const target = `${schedule.sessionName}:${schedule.windowIndex}`;
const command = `${scheduleScript} ${schedule.interval} "${schedule.note}" "${target}"`;
// Execute in background
const { stdout } = await this.tmux.executeShellCommand(command);
// Extract PID from output if available
const pidMatch = stdout.match(/PID:\s*(\d+)/);
const pid = pidMatch ? parseInt(pidMatch[1]) : undefined;
console.log(chalk_1.default.green(`✓ Scheduled check-in for ${schedule.agentId} in ${schedule.interval} minutes`));
return pid;
}
catch (error) {
console.error(chalk_1.default.red(`Failed to schedule: ${error}`));
schedule.status = 'failed';
this.stats.failed++;
this.emit('schedule-failed', { schedule, error });
return undefined;
}
}
/**
* Cancel a scheduled check-in
*/
async cancelSchedule(agentId) {
const schedule = this.schedules.get(agentId);
if (!schedule || schedule.status !== 'pending') {
return false;
}
try {
// Kill the scheduled process if we have PID
if (schedule.processId) {
await this.tmux.executeShellCommand(`kill ${schedule.processId}`);
}
schedule.status = 'cancelled';
this.stats.cancelled++;
this.emit('cancelled', schedule);
return true;
}
catch (error) {
console.error(chalk_1.default.red(`Failed to cancel schedule: ${error}`));
return false;
}
}
/**
* Reschedule an agent with new interval
*/
async reschedule(agentSession, newInterval, note) {
// Cancel existing schedule
await this.cancelSchedule(agentSession.agentId);
// Create new schedule
return this.scheduleAgentCheckIn(agentSession, newInterval, note);
}
/**
* Handle schedule execution (called when schedule fires)
*/
async handleScheduleExecution(agentId) {
const schedule = this.schedules.get(agentId);
if (!schedule) {
return;
}
schedule.status = 'executed';
this.stats.executed++;
// Emit event for orchestrator to handle
this.emit('check-in', {
agentId,
scheduledTime: schedule.scheduledTime,
actualTime: new Date(),
note: schedule.note,
});
// Log execution
console.log(chalk_1.default.blue(`Agent ${agentId} check-in executed (scheduled: ${schedule.scheduledTime.toISOString()})`));
}
/**
* Get schedule for agent
*/
getAgentSchedule(agentId) {
return this.schedules.get(agentId);
}
/**
* Get all active schedules
*/
getActiveSchedules() {
return Array.from(this.schedules.values()).filter((s) => s.status === 'pending');
}
/**
* Update average interval statistic
*/
updateAverageInterval() {
const intervals = Array.from(this.schedules.values()).map((s) => s.interval);
if (intervals.length > 0) {
this.stats.averageInterval =
intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
}
}
/**
* Get scheduler statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Cancel all active schedules
*/
async cancelAllSchedules() {
const activeSchedules = this.getActiveSchedules();
for (const schedule of activeSchedules) {
await this.cancelSchedule(schedule.agentId);
}
console.log(chalk_1.default.yellow(`Cancelled all ${activeSchedules.length} active schedules`));
}
/**
* Create recovery schedule for crashed agent
*/
async createRecoverySchedule(agentSession, error) {
const recoveryStrategy = this.config.recoveryStrategy;
console.log(chalk_1.default.yellow(`Creating recovery schedule for ${agentSession.agentId} (strategy: ${recoveryStrategy})`));
switch (recoveryStrategy) {
case 'restart':
// Schedule immediate restart
await this.scheduleAgentCheckIn(agentSession, 1, // 1 minute
`Recovery restart after error: ${error.message}`);
break;
case 'resume':
// Schedule with shorter interval
await this.scheduleAgentCheckIn(agentSession, Math.min(5, this.config.minCheckInterval), `Resume after error: ${error.message}`);
break;
case 'escalate':
// Don't reschedule, escalate to orchestrator
this.emit('escalate-recovery', {
agentSession,
error,
message: 'Agent requires manual intervention',
});
break;
}
}
/**
* Generate scheduling script content
*/
generateSchedulingScript() {
return `#!/bin/bash
# Self-scheduling script for orchestration agents
MINUTES=\${1:-${this.config.defaultCheckInterval}}
NOTE=\${2:-"Regular orchestration check"}
TARGET=\${3:-"cf-orchestrator:0"}
# Validate interval
if [ \$MINUTES -lt ${this.config.minCheckInterval} ]; then
MINUTES=${this.config.minCheckInterval}
elif [ \$MINUTES -gt ${this.config.maxCheckInterval} ]; then
MINUTES=${this.config.maxCheckInterval}
fi
# Create note file
ORCHESTRATION_DIR=".claude/orchestration"
mkdir -p "\$ORCHESTRATION_DIR"
echo "=== Next Check Note ($(date)) ===" > "\$ORCHESTRATION_DIR/next_check.txt"
echo "Scheduled for: \$MINUTES minutes" >> "\$ORCHESTRATION_DIR/next_check.txt"
echo "" >> "\$ORCHESTRATION_DIR/next_check.txt"
echo "\$NOTE" >> "\$ORCHESTRATION_DIR/next_check.txt"
# Calculate seconds
SECONDS=\$(( \$MINUTES * 60 ))
# Schedule the check using nohup for background execution
nohup bash -c "sleep \$SECONDS && tmux send-keys -t \$TARGET 'Time for orchestration check! cat \$ORCHESTRATION_DIR/next_check.txt' && sleep 1 && tmux send-keys -t \$TARGET Enter" > /dev/null 2>&1 &
# Get the PID
SCHEDULE_PID=$!
# Log the schedule
echo "Scheduled check in \$MINUTES minutes (PID: \$SCHEDULE_PID)"
echo "Target: \$TARGET"
echo "Note: \$NOTE"
# Save PID for potential cancellation
echo "\$SCHEDULE_PID" > "\$ORCHESTRATION_DIR/schedule_\${TARGET//:/}_pid.txt"
`;
}
}
exports.SelfSchedulingService = SelfSchedulingService;
//# sourceMappingURL=selfScheduler.js.map