@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
367 lines • 13.7 kB
JavaScript
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { Logger } from "../core/logger.js";
import { generateId } from "../utils/helpers.js";
import * as fs from "node:fs/promises";
import * as path from "node:path";
export class BackgroundExecutor extends EventEmitter {
logger;
config;
tasks;
processes;
queue;
isRunning = false;
checkTimer;
cleanupTimer;
constructor(config = {}) {
super();
this.logger = new Logger({
level: "info",
format: "json",
destination: "console",
});
this.config = {
maxConcurrentTasks: 5,
defaultTimeout: 300000, // 5 minutes
logPath: "./background-tasks",
enablePersistence: true,
checkInterval: 1000, // 1 second
cleanupInterval: 60000, // 1 minute
maxRetries: 3,
...config,
};
this.tasks = new Map();
this.processes = new Map();
this.queue = [];
}
async start() {
if (this.isRunning)
return;
this.logger.info("Starting background executor...");
this.isRunning = true;
// Create log directory
if (this.config.enablePersistence) {
await fs.mkdir(this.config.logPath, { recursive: true });
}
// Start background processing
this.checkTimer = setInterval(() => {
void this.processQueue();
this.checkRunningTasks();
}, this.config.checkInterval);
// Start cleanup timer
this.cleanupTimer = setInterval(() => {
this.cleanupCompletedTasks();
}, this.config.cleanupInterval);
this.emit("executor:started");
}
stop() {
if (!this.isRunning)
return;
this.logger.info("Stopping background executor...");
this.isRunning = false;
// Clear timers
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = undefined;
}
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
// Kill all running processes
for (const [taskId, process] of this.processes) {
this.logger.warn(`Killing process for task ${taskId}`);
process.kill("SIGTERM");
}
this.emit("executor:stopped");
}
async submitTask(type, command, args = [], options = {}) {
const taskId = generateId("bgtask");
const task = {
id: taskId,
type,
command,
args,
options: {
timeout: this.config.defaultTimeout,
retries: this.config.maxRetries,
...options,
},
status: "pending",
retryCount: 0,
};
this.tasks.set(taskId, task);
this.queue.push(taskId);
if (this.config.enablePersistence) {
await this.saveTaskState(task);
}
this.logger.info(`Submitted background task: ${taskId} - ${command}`);
this.emit("task:submitted", task);
// Process immediately if possible
void this.processQueue();
return taskId;
}
async submitClaudeTask(prompt, tools = [], options = {}) {
// Build claude command arguments
const args = ["-p", prompt];
if (tools.length > 0) {
args.push("--allowedTools", tools.join(","));
}
if (options.model) {
args.push("--model", options.model);
}
if (options.maxTokens) {
args.push("--max-tokens", options.maxTokens.toString());
}
args.push("--dangerously-skip-permissions");
return this.submitTask("claude-spawn", "claude", args, {
...options,
detached: true, // Run in background
});
}
async processQueue() {
if (!this.isRunning)
return;
// Check how many tasks are running
const runningTasks = Array.from(this.tasks.values())
.filter(t => t.status === "running").length;
const availableSlots = this.config.maxConcurrentTasks - runningTasks;
// Process pending tasks
for (let i = 0; i < availableSlots && this.queue.length > 0; i++) {
const taskId = this.queue.shift();
if (!taskId)
continue;
const task = this.tasks.get(taskId);
if (!task || task.status !== "pending")
continue;
await this.executeTask(task);
}
}
async executeTask(task) {
try {
task.status = "running";
task.startTime = new Date();
this.logger.info(`Executing task ${task.id}: ${task.command} ${task.args.join(" ")}`);
// Create log files for task
const logDir = path.join(this.config.logPath, task.id);
if (this.config.enablePersistence) {
await fs.mkdir(logDir, { recursive: true });
}
// Spawn process
const childProcess = spawn(task.command, task.args, {
cwd: task.options?.cwd,
env: { ...process.env, ...task.options?.env },
detached: task.options?.detached,
stdio: ["ignore", "pipe", "pipe"],
});
task.pid = childProcess.pid;
this.processes.set(task.id, childProcess);
// Collect output
let stdout = "";
let stderr = "";
childProcess.stdout?.on("data", (data) => {
stdout += data.toString();
this.emit("task:output", { taskId: task.id, data: data.toString() });
});
childProcess.stderr?.on("data", (data) => {
stderr += data.toString();
this.emit("task:error", { taskId: task.id, data: data.toString() });
});
// Handle process completion
childProcess.on("close", async (code) => {
task.endTime = new Date();
task.output = stdout;
task.error = stderr;
if (code === 0) {
task.status = "completed";
this.logger.info(`Task ${task.id} completed successfully`);
this.emit("task:completed", task);
}
else {
task.status = "failed";
this.logger.error(`Task ${task.id} failed with code ${code}`);
// Retry logic
if (task.retryCount < (task.options?.retries || 0)) {
task.retryCount++;
task.status = "pending";
this.queue.push(task.id);
this.logger.info(`Retrying task ${task.id} (${task.retryCount}/${task.options?.retries})`);
this.emit("task:retry", task);
}
else {
this.emit("task:failed", task);
}
}
this.processes.delete(task.id);
if (this.config.enablePersistence) {
await this.saveTaskOutput(task);
}
});
// Set timeout if specified
if (task.options?.timeout) {
setTimeout(() => {
if (this.processes.has(task.id)) {
this.logger.warn(`Task ${task.id} timed out after ${task.options?.timeout}ms`);
const childProcess = this.processes.get(task.id);
childProcess?.kill("SIGTERM");
}
}, task.options.timeout);
}
// For detached processes, unref to allow main process to exit
if (task.options?.detached) {
childProcess.unref();
}
this.emit("task:started", task);
if (this.config.enablePersistence) {
await this.saveTaskState(task);
}
}
catch (error) {
task.status = "failed";
task.error = String(error);
task.endTime = new Date();
this.logger.error(`Failed to execute task ${task.id}:`, error);
this.emit("task:failed", task);
if (this.config.enablePersistence) {
await this.saveTaskState(task);
}
}
}
checkRunningTasks() {
// Check for hung or timed out tasks
const now = Date.now();
for (const [taskId, task] of this.tasks) {
if (task.status !== "running" || !task.startTime)
continue;
const runtime = now - task.startTime.getTime();
const timeout = task.options?.timeout || this.config.defaultTimeout;
if (runtime > timeout) {
const process = this.processes.get(taskId);
if (process) {
this.logger.warn(`Killing timed out task ${taskId}`);
process.kill("SIGTERM");
// Force kill after 5 seconds
setTimeout(() => {
if (this.processes.has(taskId)) {
process.kill("SIGKILL");
}
}, 5000);
}
}
}
}
cleanupCompletedTasks() {
const cutoffTime = Date.now() - 3600000; // 1 hour
for (const [taskId, task] of this.tasks) {
if (task.status === "completed" || task.status === "failed") {
if (task.endTime && task.endTime.getTime() < cutoffTime) {
this.tasks.delete(taskId);
this.logger.debug(`Cleaned up old task: ${taskId}`);
}
}
}
}
async saveTaskState(task) {
if (!this.config.enablePersistence)
return;
try {
const taskFile = path.join(this.config.logPath, task.id, "task.json");
await fs.writeFile(taskFile, JSON.stringify(task, null, 2));
}
catch (error) {
this.logger.error(`Failed to save task state for ${task.id}:`, error);
}
}
async saveTaskOutput(task) {
if (!this.config.enablePersistence)
return;
try {
const logDir = path.join(this.config.logPath, task.id);
if (task.output) {
await fs.writeFile(path.join(logDir, "stdout.log"), task.output);
}
if (task.error) {
await fs.writeFile(path.join(logDir, "stderr.log"), task.error);
}
// Save final task state
await this.saveTaskState(task);
}
catch (error) {
this.logger.error(`Failed to save task output for ${task.id}:`, error);
}
}
// Public API methods
getTask(taskId) {
return this.tasks.get(taskId);
}
getTasks(status) {
const tasks = Array.from(this.tasks.values());
return status ? tasks.filter(t => t.status === status) : tasks;
}
async waitForTask(taskId, timeout) {
return new Promise((resolve, reject) => {
const task = this.tasks.get(taskId);
if (!task) {
reject(new Error("Task not found"));
return;
}
if (task.status === "completed" || task.status === "failed") {
resolve(task);
return;
}
const timeoutHandle = timeout ? setTimeout(() => {
reject(new Error("Wait timeout"));
}, timeout) : undefined;
const checkTask = () => {
const currentTask = this.tasks.get(taskId);
if (!currentTask) {
if (timeoutHandle)
clearTimeout(timeoutHandle);
reject(new Error("Task disappeared"));
return;
}
if (currentTask.status === "completed" || currentTask.status === "failed") {
if (timeoutHandle)
clearTimeout(timeoutHandle);
resolve(currentTask);
}
else {
setTimeout(checkTask, 100);
}
};
checkTask();
});
}
killTask(taskId) {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error("Task not found");
}
const process = this.processes.get(taskId);
if (process) {
this.logger.info(`Killing task ${taskId}`);
process.kill("SIGTERM");
// Force kill after 5 seconds
setTimeout(() => {
if (this.processes.has(taskId)) {
process.kill("SIGKILL");
}
}, 5000);
}
task.status = "failed";
task.error = "Task killed by user";
task.endTime = new Date();
this.emit("task:killed", task);
}
getStatus() {
const tasks = Array.from(this.tasks.values());
return {
running: tasks.filter(t => t.status === "running").length,
pending: tasks.filter(t => t.status === "pending").length,
completed: tasks.filter(t => t.status === "completed").length,
failed: tasks.filter(t => t.status === "failed").length,
queueLength: this.queue.length,
};
}
}
//# sourceMappingURL=background-executor.js.map