aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
917 lines (816 loc) • 31.8 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Config from './config.mjs';
import FileWatcher from './file-watcher.mjs';
import CronScheduler from './cron-scheduler.mjs';
import EventRouter from './event-router.mjs';
import { IPCServer } from './ipc-server.mjs';
import { AgentSupervisor } from './agent-supervisor.mjs';
import { TaskStore } from './task-store.mjs';
import { AutomationEngine } from './automation-engine.mjs';
import { registerOpsHandlers } from './ipc-ops-handlers.mjs';
import { DaemonSupervisor } from '../ralph-external/daemon-supervisor.mjs';
import { BehaviorRegistry } from '../ralph-external/lib/behavior-registry.mjs';
import { WebServer } from './web-server.mjs';
import { MessageRouter } from './message-router.mjs';
import { ScheduledTaskRunner } from './scheduled-task-runner.mjs';
import { AutonomousEngine } from './autonomous-engine.mjs';
import { MemoryManager } from './memory-manager.mjs';
import { ProviderWatcher } from './provider-watcher.mjs';
import { DaemonBehaviorLoader } from './behavior-loader.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
class DaemonMain {
constructor() {
this.config = null;
this.fileWatcher = null;
this.cronScheduler = null;
this.eventRouter = null;
this.messagingBus = null;
this.ipcServer = null;
this.supervisor = null;
this.taskStore = null;
this.automationEngine = null;
this.daemonSupervisor = null;
this.behaviorRegistry = null;
this.webServer = null;
this.messageRouter = null;
this.opsHandlersApi = null;
this.scheduledTaskRunner = null;
this.autonomousEngine = null;
this.roomManager = null;
this.memoryManager = null;
this.providerWatcher = null;
this.behaviorLoader = null;
this.shutdownInProgress = false;
this.isRotating = false;
this.startTime = Date.now();
this.pidFile = '.aiwg/daemon.pid';
this.lockFile = '.aiwg/daemon/.lock';
this.heartbeatFile = '.aiwg/daemon/heartbeat';
this.stateFile = '.aiwg/daemon/state.json';
this.logFile = '.aiwg/daemon/daemon.log';
this.socketPath = '.aiwg/daemon/daemon.sock';
this.taskStorePath = '.aiwg/daemon/tasks.json';
this.lockFd = null;
}
async start() {
try {
this.setupDirectories();
this.acquireLock();
this.writePidFile();
this.setupSignalHandlers();
this.redirectLogs();
this.loadConfig();
await this.initializeSubsystems();
this.startHeartbeat();
this.log('Daemon started successfully');
} catch (error) {
this.log(`Fatal error during startup: ${error.message}`);
process.exit(1);
}
}
setupDirectories() {
const dirs = [
'.aiwg/daemon',
'.aiwg/daemon/actions',
'.aiwg/daemon/events',
'.aiwg/daemon/memory',
];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
}
acquireLock() {
try {
this.lockFd = fs.openSync(this.lockFile, 'wx');
fs.writeSync(this.lockFd, String(process.pid));
} catch (error) {
if (error.code === 'EEXIST') {
this.log('Daemon is already running (lock file exists)');
process.exit(1);
}
throw error;
}
}
writePidFile() {
const tmpFile = `${this.pidFile}.tmp`;
fs.writeFileSync(tmpFile, String(process.pid));
fs.renameSync(tmpFile, this.pidFile);
}
setupSignalHandlers() {
process.on('SIGTERM', () => this.shutdown('SIGTERM'));
process.on('SIGINT', () => this.shutdown('SIGINT'));
process.on('SIGHUP', () => this.reloadConfig());
}
redirectLogs() {
const logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
process.stdout.write = (chunk) => {
this.rotateLogIfNeeded();
return logStream.write(chunk);
};
process.stderr.write = (chunk) => {
this.rotateLogIfNeeded();
return logStream.write(chunk);
};
}
rotateLogIfNeeded() {
if (this.isRotating) return;
try {
const stats = fs.statSync(this.logFile);
const maxSize = (this.config?.get('daemon.log.max_size_mb') || 50) * 1024 * 1024;
const maxFiles = this.config?.get('daemon.log.max_files') || 5;
if (stats.size > maxSize) {
this.isRotating = true;
for (let i = maxFiles - 1; i >= 1; i--) {
const oldFile = `${this.logFile}.${i}`;
const newFile = `${this.logFile}.${i + 1}`;
if (fs.existsSync(oldFile)) {
fs.renameSync(oldFile, newFile);
}
}
fs.renameSync(this.logFile, `${this.logFile}.1`);
if (fs.existsSync(`${this.logFile}.${maxFiles + 1}`)) {
fs.unlinkSync(`${this.logFile}.${maxFiles + 1}`);
}
this.isRotating = false;
}
} catch (error) {
this.isRotating = false;
}
}
loadConfig() {
this.config = new Config();
this.config.load();
const errors = this.config.validate();
if (errors.length > 0) {
this.log(`Configuration validation errors: ${errors.join(', ')}`);
throw new Error('Invalid configuration');
}
}
reloadConfig() {
this.log('Reloading configuration (SIGHUP received)');
try {
this.loadConfig();
this.stopSubsystems();
this.initializeSubsystems();
this.log('Configuration reloaded successfully');
} catch (error) {
this.log(`Failed to reload configuration: ${error.message}`);
}
}
async initializeSubsystems() {
// Initialize memory manager (cross-session context for concierge)
this.memoryManager = new MemoryManager({ projectRoot: process.cwd() });
const memorySnapshot = this.memoryManager.load();
const totalEntries = memorySnapshot.project.length + memorySnapshot.user.length;
this.log(`Memory manager initialized (${totalEntries} persistent entries loaded)`);
this.eventRouter = new EventRouter();
// Initialize task store (persistent task tracking)
this.taskStore = new TaskStore(this.taskStorePath);
await this.taskStore.initialize();
this.log(`Task store initialized (${this.taskStore.size} existing tasks)`);
// Initialize agent supervisor (spawns claude -p subprocesses)
const maxConcurrency = this.config.get('daemon.max_parallel_actions') || 3;
const taskTimeoutMs = (this.config.get('daemon.action_timeout_minutes') || 120) * 60 * 1000;
this.supervisor = new AgentSupervisor({
maxConcurrency,
taskStore: this.taskStore,
taskTimeout: taskTimeoutMs,
});
this.supervisor.on('task:started', (e) => this.log(`Agent task started: ${e.taskId} (PID ${e.pid})`));
this.supervisor.on('task:completed', (e) => this.log(`Agent task completed: ${e.taskId} (${e.duration}ms)`));
this.supervisor.on('task:failed', (e) => this.log(`Agent task failed: ${e.taskId} - ${e.error}`));
this.supervisor.on('task:timeout', (e) => this.log(`Agent task timed out: ${e.taskId}`));
this.log(`Agent supervisor started (max ${maxConcurrency} concurrent)`);
// Wrap AgentSupervisor with DaemonSupervisor for production governance
const supConfig = this.config.get('supervisor') || {};
this.daemonSupervisor = new DaemonSupervisor({
agentSupervisor: this.supervisor,
maxConcurrent: supConfig.max_concurrent ?? maxConcurrency,
maxQueueDepth: supConfig.max_queue_depth ?? 20,
dailyBudgetUsd: supConfig.daily_budget_usd ?? 0,
restartIntensity: {
maxRestarts: supConfig.restart_intensity?.max_restarts ?? 3,
windowMs: (supConfig.restart_intensity?.window_seconds ?? 300) * 1000,
},
circuitBreaker: supConfig.circuit_breaker,
});
this.daemonSupervisor.on('loop:started', (e) => this.log(`Loop started: ${e.loopId}`));
this.daemonSupervisor.on('loop:completed', (e) => this.log(`Loop completed: ${e.loopId}`));
this.daemonSupervisor.on('loop:failed', (e) => this.log(`Loop failed: ${e.loopId} (permanent: ${e.permanent})`));
this.daemonSupervisor.on('circuit:trip', (e) => this.log(`Circuit breaker tripped: ${e.consecutiveFailures} failures`));
this.daemonSupervisor.on('budget:warning', (e) => this.log(`Budget warning: ${e.percentUsed}% used`));
this.log('DaemonSupervisor initialized');
// Load behavior registry
try {
const projectRoot = process.cwd();
this.behaviorRegistry = new BehaviorRegistry({
projectRoot,
frameworkRoot: projectRoot,
});
const behaviors = await this.behaviorRegistry.loadAll();
this.log(`Behavior registry loaded ${behaviors.size} behavior(s)`);
} catch (err) {
this.log(`Behavior registry init failed (non-fatal): ${err.message}`);
}
// Load and activate daemon behaviors (injectable orchestrators)
try {
this.behaviorLoader = new DaemonBehaviorLoader({
projectRoot: process.cwd(),
supervisor: this.supervisor,
memoryManager: this.memoryManager,
provider: this.config.get('provider') || null,
config: this.config,
});
const active = await this.behaviorLoader.loadAll();
if (active.size > 0) {
this.log(`Daemon behaviors activated: ${[...active.keys()].join(', ')}`);
}
} catch (err) {
this.log(`Behavior loader init failed (non-fatal): ${err.message}`);
}
// Initialize MessageRouter
this.messageRouter = new MessageRouter({ supervisor: this.supervisor });
this.log('MessageRouter initialized');
// Initialize automation engine (trigger-action rules)
this.automationEngine = new AutomationEngine({
supervisor: this.supervisor,
});
const rules = this.config.get('rules') || [];
if (rules.length > 0) {
this.automationEngine.loadRules(rules);
this.log(`Automation engine loaded ${this.automationEngine.ruleCount} rules`);
}
// Initialize IPC server (Unix domain socket for CLI communication)
this.ipcServer = new IPCServer(this.socketPath);
this._registerIPCMethods();
await this.ipcServer.start();
this.log(`IPC server listening on ${this.socketPath}`);
const watchConfig = this.config.get('watch');
if (watchConfig?.enabled) {
this.fileWatcher = new FileWatcher(watchConfig, watchConfig.debounce_ms);
this.fileWatcher.on('event', (event) => this.eventRouter.route(event));
this.fileWatcher.start();
this.log('File watcher started');
}
const scheduleConfig = this.config.get('schedule');
if (scheduleConfig?.enabled) {
this.cronScheduler = new CronScheduler(scheduleConfig);
this.cronScheduler.on('event', (event) => this.eventRouter.route(event));
this.cronScheduler.start();
this.log('Cron scheduler started');
}
// Initialize RoomManager for multi-room messaging
try {
const { RoomManager } = await import('../messaging/room-manager.mjs');
this.roomManager = new RoomManager();
const messagingConfig = this.config.get('messaging');
if (messagingConfig) {
this.roomManager.loadFromConfig(messagingConfig);
this.log(`RoomManager loaded ${this.roomManager.size} room(s)`);
}
} catch (error) {
this.log(`RoomManager init failed (non-fatal): ${error.message}`);
}
// Initialize messaging subsystem if any adapters are configured
try {
const { createMessagingHub } = await import('../messaging/index.mjs');
const messagingConfig = this.config.get('messaging');
this.messagingBus = await createMessagingHub({
roomManager: this.roomManager,
messagingConfig,
});
if (this.messagingBus) {
this.log(`Messaging hub initialized with ${this.messagingBus.adapterCount} adapter(s)`);
}
} catch (error) {
this.log(`Messaging subsystem not available: ${error.message}`);
}
// Initialize ScheduledTaskRunner to bridge cron events to tasks
if (this.daemonSupervisor && this.eventRouter) {
this.scheduledTaskRunner = new ScheduledTaskRunner({
supervisor: this.daemonSupervisor,
eventRouter: this.eventRouter,
roomManager: this.roomManager,
});
this.scheduledTaskRunner.start();
this.log('ScheduledTaskRunner started');
}
// Initialize AutonomousEngine if enabled
const modesConfig = this.config.get('modes');
if (modesConfig?.autonomous && this.daemonSupervisor) {
this.autonomousEngine = new AutonomousEngine({
supervisor: this.daemonSupervisor,
config: modesConfig.autonomous,
roomManager: this.roomManager,
});
if (modesConfig.autonomous.enabled) {
this.autonomousEngine.start();
this.log('AutonomousEngine started');
} else {
this.log('AutonomousEngine initialized (disabled — enable via config or IPC)');
}
}
// Initialize ProviderWatcher if configured
const providerWatchConfig = this.config.get('provider_watch');
if (providerWatchConfig?.enabled) {
this.providerWatcher = new ProviderWatcher({
stateDir: '.aiwg/daemon/provider-watch',
providers: providerWatchConfig.providers || undefined,
intervalHours: providerWatchConfig.interval_hours || 6,
});
this.providerWatcher.on('changes-detected', (e) => {
this.log(`Provider changes detected: ${e.changes.length} change(s)`);
this.eventRouter.route({
source: 'provider-watch',
type: 'provider.changes',
payload: e,
});
});
this.providerWatcher.on('error', (e) => {
this.log(`Provider watcher error (${e.provider}): ${e.error}`);
});
this.providerWatcher.start();
// Register default automation rule for PR creation on provider changes
if (this.automationEngine) {
const existingRule = this.automationEngine.getRule('provider-watch-pr');
if (!existingRule) {
const prRule = ProviderWatcher.getAutomationRule();
this.automationEngine.loadRules([
...this.automationEngine.rules,
prRule,
]);
this.log('Registered provider-watch-pr automation rule');
}
}
this.log(`ProviderWatcher initialized (${this.providerWatcher.providers.length} providers, every ${this.providerWatcher.intervalHours}h)`);
}
// Start web server if configured
const webConfig = this.config.get('interface.web');
if (webConfig?.enabled) {
// In Docker mode, bind to 0.0.0.0 so port mapping works
const webHost = process.env.AIWG_DOCKER === '1' ? '0.0.0.0' : (webConfig.host ?? '127.0.0.1');
this.webServer = new WebServer({
port: webConfig.port ?? 7474,
host: webHost,
token: webConfig.token || process.env.AIWG_WEB_TOKEN || null,
daemonSupervisor: this.daemonSupervisor,
opsHandlers: this.opsHandlersApi,
});
await this.webServer.start();
this.log(`Web server listening on ${this.webServer.url}`);
}
// Route events through automation engine and adapters
this.eventRouter.on('event', (event) => {
this.handleEvent(event);
this.automationEngine.processEvent(event);
});
// Forward DaemonSupervisor events to MessageRouter and WebServer
if (this.daemonSupervisor) {
for (const eventName of ['loop:started', 'loop:completed', 'loop:failed', 'loop:queued', 'circuit:trip', 'circuit:recover', 'budget:warning']) {
this.daemonSupervisor.on(eventName, (data) => {
if (this.messageRouter) {
this.messageRouter.broadcast({ type: eventName, data });
}
if (this.webServer) {
this.webServer.broadcastEvent(eventName, data);
}
});
}
}
}
/**
* Register IPC method handlers for CLI communication
*/
_registerIPCMethods() {
this.ipcServer.registerMethods({
// Daemon status
'daemon.status': () => ({
pid: process.pid,
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
started_at: new Date(this.startTime).toISOString(),
health: 'healthy',
subsystems: {
ipc: { clients: this.ipcServer.clientCount },
supervisor: this.supervisor.getStatus(),
automation: this.automationEngine.getStatus(),
file_watcher: this.fileWatcher?.getStats() || { enabled: false },
scheduler: this.cronScheduler?.getStats() || { enabled: false },
messaging: this.messagingBus ? { enabled: true, adapters: this.messagingBus.adapterCount } : { enabled: false },
provider_watcher: this.providerWatcher ? { enabled: true, ...this.providerWatcher.getStatus() } : { enabled: false },
behaviors: this.behaviorLoader ? this.behaviorLoader.getStatus() : { active: {}, count: 0 },
},
}),
// Task management
'task.submit': (params) => {
if (!params.prompt) {
const err = new Error('Missing required parameter: prompt');
err.code = 'INVALID_PARAMS';
throw err;
}
const task = this.supervisor.submit(params.prompt, {
agent: params.agent,
priority: params.priority || 0,
});
return { taskId: task.id, state: task.state };
},
'task.cancel': (params) => {
if (!params.taskId) {
const err = new Error('Missing required parameter: taskId');
err.code = 'INVALID_PARAMS';
throw err;
}
const cancelled = this.supervisor.cancel(params.taskId);
return { cancelled };
},
'task.list': (params) => {
return this.taskStore.getTasks({
state: params?.state,
limit: params?.limit || 20,
});
},
'task.get': (params) => {
if (!params.taskId) {
const err = new Error('Missing required parameter: taskId');
err.code = 'INVALID_PARAMS';
throw err;
}
const task = this.taskStore.getTask(params.taskId);
if (!task) {
const err = new Error(`Task not found: ${params.taskId}`);
err.code = 'INVALID_PARAMS';
throw err;
}
return task;
},
'task.stats': () => {
return this.taskStore.getStats();
},
// Automation
'automation.status': () => {
return this.automationEngine.getStatus();
},
'automation.enable': (params) => {
if (params.ruleId) {
return { success: this.automationEngine.setRuleEnabled(params.ruleId, true) };
}
this.automationEngine.setEnabled(true);
return { enabled: true };
},
'automation.disable': (params) => {
if (params.ruleId) {
return { success: this.automationEngine.setRuleEnabled(params.ruleId, false) };
}
this.automationEngine.setEnabled(false);
return { enabled: false };
},
// Chat (send message via IPC for non-tmux clients)
// Routes through active chat-message behaviors when available.
'chat.send': async (params) => {
if (!params.message) {
const err = new Error('Missing required parameter: message');
err.code = 'INVALID_PARAMS';
throw err;
}
// Route through behaviors registered for chat-message trigger
if (this.behaviorLoader && !params.raw) {
const handlers = this.behaviorLoader.getForTrigger('chat-message');
for (const handler of handlers) {
if (typeof handler.handleMessage === 'function') {
try {
return await handler.handleMessage(params.message, {
source: 'ipc-chat',
provider: params.provider,
});
} catch (err) {
this.log(`Behavior chat handler error (falling back): ${err.message}`);
}
}
}
}
// Fallback: direct supervisor submit (no behavior mediation)
const task = this.supervisor.submit(params.message, {
priority: params.priority || 5,
metadata: { source: 'ipc-chat' },
});
return { taskId: task.id };
},
// Ping for health checks
'ping': () => ({ pong: true, timestamp: new Date().toISOString() }),
// Memory management (cross-session context)
'memory.show': (params) => {
if (!this.memoryManager) return { error: 'Memory manager not initialized', available: false };
return this.memoryManager.show(params?.scope);
},
'memory.clear': (params) => {
if (!this.memoryManager) return { error: 'Memory manager not initialized', available: false };
const scope = params?.scope;
if (!scope) {
const err = new Error('Missing required parameter: scope');
err.code = 'INVALID_PARAMS';
throw err;
}
return this.memoryManager.clear(scope);
},
'memory.write': (params) => {
if (!this.memoryManager) return { error: 'Memory manager not initialized', available: false };
const { scope, key, content, name, description, type } = params || {};
if (!scope || !key || !content) {
const err = new Error('Missing required parameters: scope, key, content');
err.code = 'INVALID_PARAMS';
throw err;
}
return this.memoryManager.write(scope, key, content, { name, description, type });
},
'memory.context': () => {
if (!this.memoryManager) return { context: '', available: false };
return { context: this.memoryManager.getContext(), available: true };
},
// Behavior management (injectable daemon behaviors)
'behaviors.status': () => {
return this.behaviorLoader ? this.behaviorLoader.getStatus() : { active: {}, count: 0 };
},
'behaviors.list': () => {
if (!this.behaviorLoader) return { behaviors: [] };
const status = this.behaviorLoader.getStatus();
return { behaviors: Object.entries(status.active).map(([name, info]) => ({ name, ...info })) };
},
'behaviors.apply': async (params) => {
if (!params?.name) {
const err = new Error('Missing required parameter: name');
err.code = 'INVALID_PARAMS';
throw err;
}
if (!this.behaviorLoader) {
const err = new Error('Behavior loader not initialized');
err.code = 'INVALID_STATE';
throw err;
}
return await this.behaviorLoader.apply(params.name);
},
'behaviors.remove': (params) => {
if (!params?.name) {
const err = new Error('Missing required parameter: name');
err.code = 'INVALID_PARAMS';
throw err;
}
if (!this.behaviorLoader) {
return { removed: false, name: params.name };
}
return this.behaviorLoader.remove(params.name);
},
// Autonomous mode management
'autonomous.status': () => {
return this.autonomousEngine ? this.autonomousEngine.getStatus() : { enabled: false };
},
'autonomous.enable': () => {
if (!this.autonomousEngine) {
const err = new Error('Autonomous engine not initialized');
err.code = 'INVALID_STATE';
throw err;
}
this.autonomousEngine.enable();
return { enabled: true };
},
'autonomous.disable': () => {
if (!this.autonomousEngine) {
const err = new Error('Autonomous engine not initialized');
err.code = 'INVALID_STATE';
throw err;
}
this.autonomousEngine.disable();
return { enabled: false };
},
// Scheduled task runner
'schedule.status': () => {
return this.scheduledTaskRunner ? this.scheduledTaskRunner.getStatus() : { started: false };
},
// Provider watch
'provider-watch.status': () => {
return this.providerWatcher ? this.providerWatcher.getStatus() : { enabled: false };
},
'provider-watch.check': async () => {
if (!this.providerWatcher) {
const err = new Error('Provider watcher not initialized — enable provider_watch in daemon config');
err.code = 'INVALID_STATE';
throw err;
}
const changes = await this.providerWatcher.checkAll();
return { changes, count: changes.length };
},
// Room management
'rooms.list': () => {
return this.roomManager ? this.roomManager.getStatus() : { totalRooms: 0 };
},
});
// Register ops-toolset IPC methods
if (this.daemonSupervisor) {
this.opsHandlersApi = registerOpsHandlers(this.ipcServer, this.daemonSupervisor);
this.log('Ops-toolset IPC methods registered (7 methods)');
}
}
handleEvent(event) {
this.log(`Event received: ${event.source} - ${event.type}`);
// Forward daemon events to messaging bus if available
if (this.messagingBus) {
this.messagingBus.publish({
topic: event.type,
source: event.source || 'daemon',
severity: 'info',
summary: `${event.source}: ${event.type}`,
details: event.payload || {},
project: path.basename(process.cwd()),
timestamp: new Date().toISOString(),
});
}
}
startHeartbeat() {
const intervalSeconds = this.config.get('daemon.heartbeat_interval_seconds') || 30;
setInterval(() => {
this.writeHeartbeat();
this.writeState();
}, intervalSeconds * 1000);
this.writeHeartbeat();
this.writeState();
}
writeHeartbeat() {
const heartbeat = {
pid: process.pid,
timestamp: new Date().toISOString(),
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000)
};
const tmpFile = `${this.heartbeatFile}.tmp`;
fs.writeFileSync(tmpFile, JSON.stringify(heartbeat, null, 2));
fs.renameSync(tmpFile, this.heartbeatFile);
}
writeState() {
const supervisorStatus = this.supervisor ? this.supervisor.getStatus() : { running: 0, queued: 0, tasks: { running: [], queued: [] } };
const daemonStatus = this.daemonSupervisor ? this.daemonSupervisor.status() : null;
const taskStats = this.taskStore ? this.taskStore.getStats() : {};
const state = {
pid: process.pid,
started_at: new Date(this.startTime).toISOString(),
last_heartbeat: new Date().toISOString(),
uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
restart_count: 0,
ipc: {
socket: this.socketPath,
clients: this.ipcServer ? this.ipcServer.clientCount : 0,
},
agents: {
running: supervisorStatus.running,
queued: supervisorStatus.queued,
max_concurrency: supervisorStatus.maxConcurrency,
tasks: supervisorStatus.tasks,
},
task_stats: taskStats,
monitors: {
file_watcher: this.fileWatcher?.getStats() || { enabled: false },
webhook: { enabled: false },
scheduler: this.cronScheduler?.getStats() || { enabled: false }
},
automation: this.automationEngine ? {
enabled: this.automationEngine.enabled,
rule_count: this.automationEngine.ruleCount,
} : { enabled: false },
daemon_supervisor: daemonStatus ? {
concurrency: `${daemonStatus.concurrencyUsed}/${daemonStatus.concurrencyMax}`,
queue: `${daemonStatus.queueDepth}/${daemonStatus.queueMax}`,
circuit: daemonStatus.circuitState?.state || 'unknown',
budget: daemonStatus.budgetLimit > 0
? `$${daemonStatus.budgetUsed.toFixed(2)}/$${daemonStatus.budgetLimit.toFixed(2)}`
: 'unlimited',
} : null,
web_server: this.webServer ? { url: this.webServer.url } : null,
rooms: this.roomManager ? this.roomManager.getStatus() : null,
scheduled_tasks: this.scheduledTaskRunner ? this.scheduledTaskRunner.getStatus() : null,
autonomous: this.autonomousEngine ? this.autonomousEngine.getStatus() : null,
health: {
status: 'healthy',
last_check: new Date().toISOString(),
issues: []
}
};
const tmpFile = `${this.stateFile}.tmp`;
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
fs.renameSync(tmpFile, this.stateFile);
}
stopSubsystems() {
if (this.fileWatcher) {
this.fileWatcher.stop();
this.fileWatcher = null;
}
if (this.cronScheduler) {
this.cronScheduler.stop();
this.cronScheduler = null;
}
if (this.scheduledTaskRunner) {
this.scheduledTaskRunner.stop();
this.scheduledTaskRunner = null;
}
if (this.autonomousEngine) {
this.autonomousEngine.stop();
this.autonomousEngine = null;
}
if (this.providerWatcher) {
this.providerWatcher.stop();
this.providerWatcher = null;
}
if (this.behaviorLoader) {
this.behaviorLoader = null;
}
}
async shutdown(signal) {
if (this.shutdownInProgress) {
return;
}
this.shutdownInProgress = true;
this.log(`Shutdown initiated (${signal})`);
// Stop IPC server (reject new connections)
if (this.ipcServer) {
try {
await this.ipcServer.stop();
this.log('IPC server stopped');
} catch (error) {
this.log(`Error stopping IPC server: ${error.message}`);
}
}
// Stop web server
if (this.webServer) {
try {
await this.webServer.stop();
this.log('Web server stopped');
} catch (error) {
this.log(`Error stopping web server: ${error.message}`);
}
}
// Drain DaemonSupervisor (which delegates to AgentSupervisor)
if (this.daemonSupervisor) {
try {
await this.daemonSupervisor.shutdown(15000);
this.log('DaemonSupervisor drained');
} catch (error) {
this.log(`Error draining DaemonSupervisor: ${error.message}`);
}
} else if (this.supervisor) {
try {
this.log(`Draining supervisor (${this.supervisor.runningCount} running, ${this.supervisor.queuedCount} queued)`);
await this.supervisor.shutdown(15000);
this.log('Agent supervisor drained');
} catch (error) {
this.log(`Error draining supervisor: ${error.message}`);
}
}
// Shutdown messaging adapters
if (this.messagingBus) {
try {
await this.messagingBus.shutdown();
this.log('Messaging hub shut down');
} catch (error) {
this.log(`Error shutting down messaging: ${error.message}`);
}
}
// Flush session memory to project scope before exit
if (this.memoryManager) {
try {
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
this.memoryManager.flushSession({ duration: `${uptime}s` });
this.log('Session memory flushed to project scope');
} catch (error) {
this.log(`Memory flush failed (non-fatal): ${error.message}`);
}
}
this.stopSubsystems();
this.writeState();
this.cleanup();
this.log('Daemon stopped gracefully');
process.exit(0);
}
cleanup() {
try {
if (fs.existsSync(this.pidFile)) {
fs.unlinkSync(this.pidFile);
}
} catch (error) {
this.log(`Failed to remove PID file: ${error.message}`);
}
try {
if (this.lockFd !== null) {
fs.closeSync(this.lockFd);
fs.unlinkSync(this.lockFile);
}
} catch (error) {
this.log(`Failed to remove lock file: ${error.message}`);
}
}
log(message) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${message}`);
}
}
const daemon = new DaemonMain();
daemon.start();