Date: 2025-12-08 Goal: Identify common daemon patterns for extraction into DaemonBase to improve stability and responsiveness
After analyzing 13+ daemons for logging standardization, clear patterns emerged around:
- Event subscription management
- Interval lifecycle management
- Cleanup coordination
3 out of 13 daemons explicitly implement cleanup patterns. These patterns should be extracted into DaemonBase to:
- Reduce code duplication
- Prevent resource leaks
- Ensure consistent cleanup behavior
- Make daemon development easier
grep -r "async cleanup()" daemons/*/server/*.ts
# Found 6 files
grep -r "unsubscribe" daemons/*/server/*.ts
# Found 3 files with cleanup patternsRead and compared:
- UserDaemonServer.ts (472 lines) - Most complex lifecycle
- TrainingDaemonServer.ts (332 lines) - Event-driven cleanup
- RoomMembershipDaemonServer.ts (262 lines) - Simple cleanup
- DaemonBase has
shutdown()stub but no cleanup helpers - Every daemon reimplements event subscription tracking
- Interval management done manually in each daemon
UserDaemonServer.ts (lines 32, 76, 108, 114, 120, 459-463):
export class UserDaemonServer extends UserDaemon {
private unsubscribeFunctions: (() => void)[] = [];
// Store unsubscribe function
private subscribeToSystemReady(): void {
const unsubReady = Events.subscribe('system:ready', async (payload: any) => {
// ... handler logic ...
});
this.unsubscribeFunctions.push(unsubReady); // ← Pattern
}
// Cleanup
async shutdown(): Promise<void> {
await super.shutdown();
for (const unsubscribe of this.unsubscribeFunctions) {
unsubscribe();
}
this.unsubscribeFunctions = [];
}
}TrainingDaemonServer.ts (lines 55, 127, 324-330):
export class TrainingDaemonServer extends TrainingDaemon {
private unsubscribeFunctions: (() => void)[] = [];
private async setupEventSubscriptions(): Promise<void> {
const unsubCreated = Events.subscribe<ChatMessageEntity>(
DATA_EVENTS.CHAT_MESSAGES.CREATED,
async (messageEntity: ChatMessageEntity) => {
await this.handleMessageCreated(messageEntity);
}
);
this.unsubscribeFunctions.push(unsubCreated); // ← Pattern
}
async cleanup(): Promise<void> {
this.log.info('🧠 TrainingDaemon: Cleaning up subscriptions...');
for (const unsub of this.unsubscribeFunctions) {
unsub();
}
this.unsubscribeFunctions = [];
}
}RoomMembershipDaemonServer.ts (lines 34, 135, 255-259):
export class RoomMembershipDaemonServer extends RoomMembershipDaemon {
private unsubscribeFunctions: (() => void)[] = [];
private async setupEventSubscriptions(): Promise<void> {
const unsubCreated = Events.subscribe<UserEntity>(
DATA_EVENTS.USERS.CREATED,
async (userData: UserEntity) => {
await this.handleUserCreated(userData);
}
);
this.unsubscribeFunctions.push(unsubCreated); // ← Pattern
}
async shutdown(): Promise<void> {
this.unsubscribeFunctions.forEach(unsub => unsub());
this.unsubscribeFunctions = [];
await super.shutdown();
}
}DaemonBase.ts (proposed additions):
export abstract class DaemonBase extends JTAGModule implements MessageSubscriber {
protected log: DaemonLogger;
// NEW: Event subscription tracking
private unsubscribeFunctions: (() => void)[] = [];
/**
* Register an event subscription for automatic cleanup
* Called by subclasses when subscribing to events
*/
protected registerSubscription(unsubscribe: () => void): void {
this.unsubscribeFunctions.push(unsubscribe);
}
/**
* Unsubscribe from all registered events
* Called automatically during shutdown
*/
protected cleanupSubscriptions(): void {
this.log.debug(`Cleaning up ${this.unsubscribeFunctions.length} event subscription(s)`);
for (const unsub of this.unsubscribeFunctions) {
try {
unsub();
} catch (error) {
this.log.error(`Failed to unsubscribe:`, error);
}
}
this.unsubscribeFunctions = [];
}
async shutdown(): Promise<void> {
this.log.info(`🔄 ${this.toString()}: Shutting down...`);
// Call subclass cleanup first
await this.cleanup();
// Then automatic cleanup
this.cleanupSubscriptions();
}
/**
* Override in subclasses for custom cleanup logic
* Called before automatic cleanup in shutdown()
*/
protected async cleanup(): Promise<void> {
// Default: no-op
}
}- ✅ Eliminates duplicate code across 3+ daemons
- ✅ Prevents subscription leaks (guaranteed cleanup)
- ✅ Consistent cleanup order (subclass first, then automatic)
- ✅ Error handling for failed unsubscribes
- ✅ Debug logging for cleanup operations
Before (UserDaemonServer.ts):
export class UserDaemonServer extends UserDaemon {
private unsubscribeFunctions: (() => void)[] = [];
private subscribeToSystemReady(): void {
const unsubReady = Events.subscribe('system:ready', async (payload: any) => {
// ... handler ...
});
this.unsubscribeFunctions.push(unsubReady);
}
async shutdown(): Promise<void> {
await super.shutdown();
for (const unsubscribe of this.unsubscribeFunctions) {
unsubscribe();
}
this.unsubscribeFunctions = [];
// ... persona cleanup ...
}
}After (using DaemonBase helpers):
export class UserDaemonServer extends UserDaemon {
// No more unsubscribeFunctions field!
private subscribeToSystemReady(): void {
const unsubReady = Events.subscribe('system:ready', async (payload: any) => {
// ... handler ...
});
this.registerSubscription(unsubReady); // ← Use base class method
}
// Override cleanup for persona-specific logic
protected async cleanup(): Promise<void> {
// Shutdown all persona clients
for (const userId of this.personaClients.keys()) {
// TODO: Add shutdown method to PersonaUser
}
this.personaClients.clear();
}
// shutdown() is now inherited and automatic!
}UserDaemonServer.ts (lines 30-31, 340-356, 435-451):
export class UserDaemonServer extends UserDaemon {
private monitoringInterval?: ReturnType<typeof setInterval>;
private reconciliationInterval?: ReturnType<typeof setInterval>;
protected startMonitoringLoops(): boolean {
// User monitoring loop - every 5 seconds
this.monitoringInterval = setInterval(() => {
this.runUserMonitoringLoop().catch((error: Error) => {
this.log.error('❌ UserDaemon: Monitoring loop error:', error);
});
}, 5000);
// State reconciliation loop - every 30 seconds
this.reconciliationInterval = setInterval(() => {
this.runStateReconciliationLoop().catch((error: Error) => {
this.log.error('❌ UserDaemon: Reconciliation loop error:', error);
});
}, 30000);
return true;
}
protected stopMonitoringLoops(): boolean {
let stopped = false;
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = undefined;
stopped = true;
}
if (this.reconciliationInterval) {
clearInterval(this.reconciliationInterval);
this.reconciliationInterval = undefined;
stopped = true;
}
return stopped;
}
}DaemonBase.ts (proposed additions):
export abstract class DaemonBase extends JTAGModule implements MessageSubscriber {
// NEW: Interval tracking
private intervals: Map<string, ReturnType<typeof setInterval>> = new Map();
/**
* Register a named interval for automatic cleanup
* Replaces manual interval tracking in each daemon
*/
protected registerInterval(
name: string,
callback: () => void | Promise<void>,
intervalMs: number
): void {
// Clear existing interval with same name
this.clearInterval(name);
const interval = setInterval(() => {
const result = callback();
if (result instanceof Promise) {
result.catch((error: Error) => {
this.log.error(`Interval '${name}' error:`, error);
});
}
}, intervalMs);
this.intervals.set(name, interval);
this.log.debug(`Registered interval '${name}' (${intervalMs}ms)`);
}
/**
* Clear a specific named interval
*/
protected clearInterval(name: string): boolean {
const interval = this.intervals.get(name);
if (interval) {
clearInterval(interval);
this.intervals.delete(name);
this.log.debug(`Cleared interval '${name}'`);
return true;
}
return false;
}
/**
* Clear all registered intervals
* Called automatically during shutdown
*/
protected cleanupIntervals(): void {
this.log.debug(`Cleaning up ${this.intervals.size} interval(s)`);
for (const [name, interval] of this.intervals) {
clearInterval(interval);
this.log.debug(`Cleared interval '${name}'`);
}
this.intervals.clear();
}
async shutdown(): Promise<void> {
this.log.info(`🔄 ${this.toString()}: Shutting down...`);
// Call subclass cleanup first
await this.cleanup();
// Then automatic cleanup
this.cleanupSubscriptions();
this.cleanupIntervals(); // ← NEW
}
}- ✅ Named intervals (self-documenting)
- ✅ Automatic error handling for async callbacks
- ✅ Prevents interval leaks
- ✅ Consistent cleanup order
- ✅ Easier testing (can mock intervals by name)
Before (UserDaemonServer.ts):
export class UserDaemonServer extends UserDaemon {
private monitoringInterval?: ReturnType<typeof setInterval>;
private reconciliationInterval?: ReturnType<typeof setInterval>;
protected startMonitoringLoops(): boolean {
this.monitoringInterval = setInterval(() => {
this.runUserMonitoringLoop().catch((error: Error) => {
this.log.error('❌ UserDaemon: Monitoring loop error:', error);
});
}, 5000);
this.reconciliationInterval = setInterval(() => {
this.runStateReconciliationLoop().catch((error: Error) => {
this.log.error('❌ UserDaemon: Reconciliation loop error:', error);
});
}, 30000);
return true;
}
protected stopMonitoringLoops(): boolean {
// ... manual cleanup ...
}
}After (using DaemonBase helpers):
export class UserDaemonServer extends UserDaemon {
// No more interval fields!
protected startMonitoringLoops(): void {
this.registerInterval('user-monitoring', async () => {
await this.runUserMonitoringLoop();
}, 5000);
this.registerInterval('state-reconciliation', async () => {
await this.runStateReconciliationLoop();
}, 30000);
}
// stopMonitoringLoops() no longer needed - automatic cleanup!
}UserDaemonServer.ts (lines 65-83):
export class UserDaemonServer extends UserDaemon {
private subscribeToSystemReady(): void {
const unsubReady = Events.subscribe('system:ready', async (payload: any) => {
if (payload?.daemon === 'data') {
this.log.info('📡 UserDaemon: Received system:ready from DataDaemon, initializing personas...');
await this.ensurePersonaClients().catch((error: Error) => {
this.log.error('❌ UserDaemon: Failed to initialize persona clients:', error);
});
}
});
this.unsubscribeFunctions.push(unsubReady);
// Initialize ToolRegistry immediately
this.initializeToolRegistry().catch((error: Error) => {
this.log.error('❌ UserDaemon: Failed to initialize ToolRegistry:', error);
});
}
}RoomMembershipDaemonServer.ts (lines 79-84):
export class RoomMembershipDaemonServer extends RoomMembershipDaemon {
async initialize(): Promise<void> {
await this.setupEventSubscriptions();
// Defer catch-up logic until after DataDaemon is ready
setTimeout(() => {
this.ensureAllUsersInRooms().catch(error => {
this.log.error('❌ RoomMembershipDaemon: Deferred catch-up failed:', error);
});
}, 2000); // 2 second delay
}
}DaemonBase.ts (proposed additions):
export abstract class DaemonBase extends JTAGModule implements MessageSubscriber {
/**
* Wait for another daemon to be ready before executing callback
* Useful for initialization dependencies (e.g., wait for DataDaemon)
*/
protected onDaemonReady(
daemonName: string,
callback: () => Promise<void>
): void {
const unsub = Events.subscribe('system:ready', async (payload: any) => {
if (payload?.daemon === daemonName) {
this.log.info(`📡 ${this.toString()}: ${daemonName} is ready`);
try {
await callback();
} catch (error) {
this.log.error(`Failed to handle ${daemonName} ready:`, error);
}
}
});
this.registerSubscription(unsub);
}
/**
* Defer execution until after initialization completes
* Alternative to setTimeout with better logging
*/
protected deferInitialization(
callback: () => Promise<void>,
delayMs: number = 2000
): void {
setTimeout(async () => {
try {
await callback();
} catch (error) {
this.log.error('Deferred initialization failed:', error);
}
}, delayMs);
}
}- ✅ Declarative daemon dependencies
- ✅ Consistent logging for initialization
- ✅ Error handling built-in
- ✅ Replaces magic setTimeout with named intent
Before (UserDaemonServer.ts):
export class UserDaemonServer extends UserDaemon {
private subscribeToSystemReady(): void {
const unsubReady = Events.subscribe('system:ready', async (payload: any) => {
if (payload?.daemon === 'data') {
this.log.info('📡 UserDaemon: Received system:ready from DataDaemon...');
await this.ensurePersonaClients().catch((error: Error) => {
this.log.error('❌ UserDaemon: Failed to initialize persona clients:', error);
});
}
});
this.unsubscribeFunctions.push(unsubReady);
}
}After (using DaemonBase helpers):
export class UserDaemonServer extends UserDaemon {
constructor(context: JTAGContext, router: JTAGRouter) {
super(context, router);
// Declarative dependency
this.onDaemonReady('data', async () => {
await this.ensurePersonaClients();
});
}
}Goal: Add new methods without changing existing daemons
Changes:
- Add
unsubscribeFunctionsfield to DaemonBase - Add
intervalsmap to DaemonBase - Add
registerSubscription()method - Add
cleanupSubscriptions()method - Add
registerInterval()method - Add
cleanupIntervals()method - Add
onDaemonReady()helper - Add
deferInitialization()helper - Enhance
shutdown()to call cleanup helpers - Add
cleanup()abstract method (optional override)
Files:
daemons/command-daemon/shared/DaemonBase.ts(~150 lines added)
Validation:
- ✅ TypeScript compilation passes
- ✅ Existing daemons unaffected (backwards compatible)
- ✅ Deploy and verify system works
Goal: Demonstrate benefits with most complex daemon
Changes:
- Remove
unsubscribeFunctionsfield - Replace
.push(unsub)withthis.registerSubscription(unsub) - Remove
monitoringIntervalandreconciliationIntervalfields - Replace
setInterval()withthis.registerInterval(name, callback, ms) - Remove
stopMonitoringLoops()method (automatic) - Simplify
shutdown()to callsuper.shutdown()only - Override
cleanup()for persona-specific logic - Replace system ready subscription with
this.onDaemonReady('data', callback)
Files:
daemons/user-daemon/server/UserDaemonServer.ts(~50 lines removed)
Validation:
- ✅ npm run build:ts passes
- ✅ Deploy and verify PersonaUsers still initialize
- ✅ Verify intervals still run
- ✅ Verify cleanup works on shutdown
Goal: Apply pattern to all daemons with cleanup
Daemons to migrate:
- TrainingDaemonServer (lines 55, 127, 324-330)
- RoomMembershipDaemonServer (lines 34, 135, 255-259)
Process:
- Migrate one daemon at a time
- Test between migrations
- Commit after each successful migration
Expected savings:
- ~30-50 lines removed per daemon
- Eliminates 3+ duplicate implementations
- Consistent cleanup behavior across all daemons
Goal: Make pattern standard for all future daemons
Updates:
- Update
docs/ARCHITECTURE-RULES.mdwith daemon lifecycle patterns - Add section to
CLAUDE.mdabout daemon base class usage - Create daemon creation guide showing proper subscription/interval usage
- Update generator templates to use base class helpers
- Code reduction: ~150-200 lines eliminated across 3-4 daemons
- Pattern consistency: 100% of daemons use same cleanup mechanism
- Bug prevention: Zero subscription/interval leaks guaranteed
- Easier daemon development: No need to remember cleanup patterns
- Better debugging: Centralized logging for all cleanup operations
- Improved stability: Automatic cleanup prevents resource leaks
- Clearer intent: Named intervals and declarative dependencies
Risk: Breaking existing daemons during migration Mitigation: Phase 1 is backwards compatible, migrate incrementally
Risk: Introducing new bugs in base class Mitigation: Comprehensive testing with UserDaemonServer first
Risk: Over-abstraction making code harder to understand Mitigation: Keep helpers simple, document with examples
| Daemon | Has Cleanup? | Event Subscriptions? | Intervals? | Priority |
|---|---|---|---|---|
| UserDaemonServer | ✅ Yes | 3 subscriptions | 2 intervals | HIGH - Complex |
| TrainingDaemonServer | ✅ Yes | 1 subscription | None | HIGH - Pattern match |
| RoomMembershipDaemonServer | ✅ Yes | 1 subscription | None | HIGH - Pattern match |
| DataDaemonServer | ❌ No | None | None | LOW |
| CommandDaemonServer | ❌ No | None | None | LOW |
| EventsDaemonServer | ❌ No | None | None | LOW |
| SessionDaemonServer | ❌ No | None | None | LOW |
| WidgetDaemonServer | ❌ No | None | None | LOW |
| HealthDaemonServer | ❌ No | None | None | LOW |
| ProxyDaemonServer | ❌ No | None | None | LOW |
| LeaseDaemonServer | ❌ No | None | None | LOW |
| CodeDaemonServer | ❌ No | None | None | LOW |
| ConsoleDaemonServer | ❓ Special | None | None | N/A - Intentional |
Conclusion: 3 out of 13 daemons (23%) explicitly need cleanup. Extracting pattern to base class benefits these immediately and prepares for future daemons.
- Review this document - Validate patterns and approach
- Implement Phase 1 - Add helpers to DaemonBase (non-breaking)
- Test with UserDaemonServer - Prove pattern works with complex daemon
- Migrate remaining daemons - TrainingDaemonServer, RoomMembershipDaemonServer
- Update documentation - Make pattern standard for all future daemons
Estimated effort: 2-3 hours for implementation, 1 hour for testing, 1 hour for documentation
Impact: Foundation for improved daemon stability, responsiveness, and maintainability