/** * Todo-Task Synchronization Service * Implements bi-directional synchronization between TaskEngine and TodoWrite operations * Provides automatic status updates, metadata tracking, and intelligent coordination */ import { EventEmitter } from 'node:events'; import { TaskEngine, WorkflowTask } from './engine.js'; import { TaskCoordinator, TodoItem } from './coordination.js'; import { Logger } from '../core/logger.js'; import { TaskStatus } from '../utils/types.js'; export interface TodoTaskMapping { todoId: string; taskId: string; relationship: 'one-to-one' | 'parent-child' | 'todo-breakdown'; createdAt: Date; metadata: Record; } export interface SyncConfiguration { autoCreateTasks: boolean; autoCreateTodos: boolean; bidirectionalSync: boolean; preserveHistory: boolean; enableIntelligentUpdates: boolean; syncInterval: number; // milliseconds retryAttempts: number; memoryNamespace: string; } export interface TodoUpdateEvent { todoId: string; previousStatus: string; newStatus: string; timestamp: Date; triggeredBy: 'task' | 'manual' | 'system'; metadata?: Record; } export class TodoSyncService extends EventEmitter { private taskEngine: TaskEngine; private taskCoordinator: TaskCoordinator; private logger: Logger; private memoryManager?: any; private mappings = new Map(); private syncQueue = new Map(); private processingQueue = false; private syncTimer?: NodeJS.Timeout; constructor( taskEngine: TaskEngine, taskCoordinator: TaskCoordinator, memoryManager?: any, private config: Partial = {} ) { super(); this.taskEngine = taskEngine; this.taskCoordinator = taskCoordinator; this.memoryManager = memoryManager; this.logger = new Logger('TodoSyncService'); // Default configuration this.config = { autoCreateTasks: true, autoCreateTodos: true, bidirectionalSync: true, preserveHistory: true, enableIntelligentUpdates: true, syncInterval: 1000, retryAttempts: 3, memoryNamespace: 'todo_task_sync', ...config }; this.initializeEventHandlers(); this.startSyncTimer(); } /** * Initialize event handlers for bi-directional synchronization */ private initializeEventHandlers(): void { // Listen to task events this.taskEngine.on('task:created', this.handleTaskCreated.bind(this)); this.taskEngine.on('task:completed', this.handleTaskCompleted.bind(this)); this.taskEngine.on('task:failed', this.handleTaskFailed.bind(this)); this.taskEngine.on('task:cancelled', this.handleTaskCancelled.bind(this)); this.taskEngine.on('task:progress', this.handleTaskProgress.bind(this)); this.taskEngine.on('task:running', this.handleTaskStarted.bind(this)); // Listen to todo events this.taskCoordinator.on('todo:updated', this.handleTodoUpdated.bind(this)); this.taskCoordinator.on('todos:created', this.handleTodosCreated.bind(this)); this.logger.info('Todo-Task synchronization event handlers initialized'); } /** * Handle task creation - create corresponding todo if configured */ private async handleTaskCreated(data: { task: WorkflowTask }): Promise { try { const { task } = data; // Check if task already has a todo mapping const existingMappings = this.mappings.get(task.id) || []; const hasTodoMapping = existingMappings.some(m => m.relationship === 'one-to-one'); if (!hasTodoMapping && this.config.autoCreateTodos) { // Create corresponding todo const todo: TodoItem = { id: `todo-${task.id}`, content: task.description || `Task: ${task.type}`, status: this.mapTaskStatusToTodo(task.status), priority: this.mapTaskPriorityToTodo(task.priority || 0) as 'high' | 'medium' | 'low', dependencies: task.dependencies?.map(dep => `todo-${dep.taskId}`) || [], estimatedTime: task.estimatedDurationMs ? `${Math.round(task.estimatedDurationMs / 60000)}min` : undefined, assignedAgent: task.assignedAgent, batchOptimized: task.metadata?.batchOptimized as boolean || false, parallelExecution: task.metadata?.parallelExecution as boolean || false, memoryKey: `task-${task.id}`, tags: [...(task.tags || []), 'auto-generated', 'task-sync'], metadata: { sourceTaskId: task.id, autoGenerated: true, syncTimestamp: new Date() }, createdAt: new Date(), updatedAt: new Date() }; // Store todo in coordinator await this.taskCoordinator.updateTodoProgress(todo.id, todo.status, todo.metadata); // Create mapping const mapping: TodoTaskMapping = { todoId: todo.id, taskId: task.id, relationship: 'one-to-one', createdAt: new Date(), metadata: { autoCreated: true, sourceType: 'task' } }; this.addMapping(task.id, mapping); // Store in memory for persistence if (this.memoryManager) { await this.memoryManager.store(`todo:${todo.id}`, todo, { namespace: this.config.memoryNamespace, tags: ['todo', 'task-sync', 'auto-generated'] }); await this.memoryManager.store(`mapping:${task.id}`, mapping, { namespace: this.config.memoryNamespace, tags: ['mapping', 'task-sync'] }); } this.logger.info(`Auto-created todo ${todo.id} for task ${task.id}`); this.emit('todo:auto-created', { todo, task, mapping }); } // Update task metadata to include todo reference if (hasTodoMapping) { const todoMapping = existingMappings.find(m => m.relationship === 'one-to-one'); if (todoMapping) { task.metadata = { ...task.metadata, linkedTodoId: todoMapping.todoId, syncEnabled: true }; } } } catch (error) { this.logger.error('Error handling task creation', { taskId: data.task.id, error }); this.emit('sync:error', { type: 'task-creation', taskId: data.task.id, error }); } } /** * Handle task completion - update corresponding todos */ private async handleTaskCompleted(data: { taskId: string; result: unknown }): Promise { try { const { taskId, result } = data; const mappings = this.mappings.get(taskId) || []; for (const mapping of mappings) { const updateEvent: TodoUpdateEvent = { todoId: mapping.todoId, previousStatus: 'in_progress', newStatus: 'completed', timestamp: new Date(), triggeredBy: 'task', metadata: { taskResult: result, completedTaskId: taskId, autoUpdate: true } }; this.queueTodoUpdate(mapping.todoId, updateEvent); } this.logger.info(`Queued todo updates for completed task ${taskId}`); } catch (error) { this.logger.error('Error handling task completion', { taskId: data.taskId, error }); this.emit('sync:error', { type: 'task-completion', taskId: data.taskId, error }); } } /** * Handle task failure - update corresponding todos */ private async handleTaskFailed(data: { taskId: string; error: Error }): Promise { try { const { taskId, error } = data; const mappings = this.mappings.get(taskId) || []; for (const mapping of mappings) { const updateEvent: TodoUpdateEvent = { todoId: mapping.todoId, previousStatus: 'in_progress', newStatus: 'pending', // Reset to pending for retry timestamp: new Date(), triggeredBy: 'task', metadata: { taskError: error.message, failedTaskId: taskId, autoUpdate: true, requiresAttention: true } }; this.queueTodoUpdate(mapping.todoId, updateEvent); } this.logger.warn(`Queued todo updates for failed task ${taskId}`, { error: error.message }); } catch (error) { this.logger.error('Error handling task failure', { taskId: data.taskId, error }); this.emit('sync:error', { type: 'task-failure', taskId: data.taskId, error }); } } /** * Handle task cancellation - update corresponding todos */ private async handleTaskCancelled(data: { taskId: string; reason: string }): Promise { try { const { taskId, reason } = data; const mappings = this.mappings.get(taskId) || []; for (const mapping of mappings) { const updateEvent: TodoUpdateEvent = { todoId: mapping.todoId, previousStatus: 'in_progress', newStatus: 'pending', // Reset to pending timestamp: new Date(), triggeredBy: 'task', metadata: { cancellationReason: reason, cancelledTaskId: taskId, autoUpdate: true, cancelled: true } }; this.queueTodoUpdate(mapping.todoId, updateEvent); } this.logger.info(`Queued todo updates for cancelled task ${taskId}`, { reason }); } catch (error) { this.logger.error('Error handling task cancellation', { taskId: data.taskId, error }); this.emit('sync:error', { type: 'task-cancellation', taskId: data.taskId, error }); } } /** * Handle task progress updates - update corresponding todos */ private async handleTaskProgress(data: { taskId: string; progress: number; metrics: any }): Promise { try { const { taskId, progress, metrics } = data; const mappings = this.mappings.get(taskId) || []; // Only update for significant progress changes if (progress % 25 === 0 || progress >= 90) { for (const mapping of mappings) { const updateEvent: TodoUpdateEvent = { todoId: mapping.todoId, previousStatus: 'in_progress', newStatus: 'in_progress', timestamp: new Date(), triggeredBy: 'task', metadata: { progress, metrics, taskId, autoUpdate: true, progressUpdate: true } }; this.queueTodoUpdate(mapping.todoId, updateEvent); } } } catch (error) { this.logger.error('Error handling task progress', { taskId: data.taskId, error }); } } /** * Handle task started - update corresponding todos to in_progress */ private async handleTaskStarted(data: { taskId: string; agentId: string }): Promise { try { const { taskId, agentId } = data; const mappings = this.mappings.get(taskId) || []; for (const mapping of mappings) { const updateEvent: TodoUpdateEvent = { todoId: mapping.todoId, previousStatus: 'pending', newStatus: 'in_progress', timestamp: new Date(), triggeredBy: 'task', metadata: { assignedAgent: agentId, startedTaskId: taskId, autoUpdate: true } }; this.queueTodoUpdate(mapping.todoId, updateEvent); } this.logger.info(`Queued todo updates for started task ${taskId}`, { agentId }); } catch (error) { this.logger.error('Error handling task start', { taskId: data.taskId, error }); } } /** * Handle todo updates - create or update corresponding tasks */ private async handleTodoUpdated(data: { todoId: string; status: string; previousStatus: string; todo: TodoItem }): Promise { try { const { todoId, status, previousStatus, todo } = data; // Skip auto-generated updates to prevent loops if (todo.metadata?.autoUpdate) { return; } // Find corresponding task mappings const mappings = this.findMappingsByTodoId(todoId); if (mappings.length === 0 && this.config.autoCreateTasks && status === 'in_progress') { // Create new task for this todo await this.createTaskFromTodo(todo); } else { // Update existing tasks for (const mapping of mappings) { await this.updateTaskFromTodo(mapping.taskId, todo, status, previousStatus); } } this.logger.info(`Processed todo update for ${todoId}`, { status, previousStatus }); } catch (error) { this.logger.error('Error handling todo update', { todoId: data.todoId, error }); this.emit('sync:error', { type: 'todo-update', todoId: data.todoId, error }); } } /** * Handle bulk todo creation - create corresponding tasks if needed */ private async handleTodosCreated(data: { sessionId: string; todos: TodoItem[]; context: any }): Promise { try { const { todos, sessionId } = data; if (this.config.autoCreateTasks) { for (const todo of todos) { if (todo.status === 'in_progress') { await this.createTaskFromTodo(todo); } } } this.logger.info(`Processed bulk todo creation for session ${sessionId}`, { todoCount: todos.length }); } catch (error) { this.logger.error('Error handling todos creation', { sessionId: data.sessionId, error }); this.emit('sync:error', { type: 'todos-creation', sessionId: data.sessionId, error }); } } /** * Create a task from a todo item */ private async createTaskFromTodo(todo: TodoItem): Promise { try { const task = await this.taskEngine.createTask({ id: `task-${todo.id}`, type: this.inferTaskTypeFromTodo(todo), description: todo.content, priority: this.mapTodoPriorityToTask(todo.priority), dependencies: todo.dependencies?.map(depTodoId => ({ taskId: `task-${depTodoId}`, type: 'finish-to-start' as const })) || [], estimatedDurationMs: this.parseEstimatedTime(todo.estimatedTime), assignedAgent: todo.assignedAgent, tags: [...(todo.tags || []), 'todo-generated'], metadata: { sourceTodoId: todo.id, batchOptimized: todo.batchOptimized, parallelExecution: todo.parallelExecution, autoGenerated: true, syncTimestamp: new Date() } }); // Create mapping const mapping: TodoTaskMapping = { todoId: todo.id, taskId: task.id, relationship: 'one-to-one', createdAt: new Date(), metadata: { autoCreated: true, sourceType: 'todo' } }; this.addMapping(task.id, mapping); // Store in memory if (this.memoryManager) { await this.memoryManager.store(`mapping:${task.id}`, mapping, { namespace: this.config.memoryNamespace, tags: ['mapping', 'todo-sync'] }); } this.logger.info(`Auto-created task ${task.id} from todo ${todo.id}`); this.emit('task:auto-created', { task, todo, mapping }); } catch (error) { this.logger.error('Error creating task from todo', { todoId: todo.id, error }); throw error; } } /** * Update a task based on todo changes */ private async updateTaskFromTodo(taskId: string, todo: TodoItem, newStatus: string, previousStatus: string): Promise { try { const taskStatus = await this.taskEngine.getTaskStatus(taskId); if (!taskStatus) { this.logger.warn(`Task ${taskId} not found for todo update`); return; } const { task } = taskStatus; // Update task metadata task.metadata = { ...task.metadata, lastTodoUpdate: new Date(), todoStatus: newStatus, previousTodoStatus: previousStatus, syncedFromTodo: true }; // Handle status changes if (newStatus === 'completed' && task.status !== 'completed') { // Mark task as completed task.status = 'completed'; task.progressPercentage = 100; task.completedAt = new Date(); // Emit completion event this.taskEngine.emit('task:completed', { taskId, result: { completedViaTodo: true } }); } else if (newStatus === 'in_progress' && task.status === 'pending') { // Start task execution task.status = 'queued'; // Trigger scheduler this.taskEngine.emit('task:queued', { taskId }); } // Store updated task if (this.memoryManager) { await this.memoryManager.store(`task:${taskId}`, task); } this.logger.info(`Updated task ${taskId} from todo ${todo.id}`, { newStatus, previousStatus }); } catch (error) { this.logger.error('Error updating task from todo', { taskId, todoId: todo.id, error }); throw error; } } /** * Queue a todo update for batch processing */ private queueTodoUpdate(todoId: string, updateEvent: TodoUpdateEvent): void { this.syncQueue.set(todoId, updateEvent); if (!this.processingQueue) { // Process queue on next tick to allow batching process.nextTick(() => this.processSyncQueue()); } } /** * Process queued todo updates */ private async processSyncQueue(): Promise { if (this.processingQueue || this.syncQueue.size === 0) { return; } this.processingQueue = true; try { const updates = Array.from(this.syncQueue.entries()); this.syncQueue.clear(); for (const [todoId, updateEvent] of updates) { try { await this.taskCoordinator.updateTodoProgress( todoId, updateEvent.newStatus as 'pending' | 'in_progress' | 'completed', updateEvent.metadata ); // Store update history if (this.config.preserveHistory && this.memoryManager) { await this.memoryManager.store(`todo_update:${todoId}:${Date.now()}`, updateEvent, { namespace: this.config.memoryNamespace, tags: ['todo-update', 'history', updateEvent.triggeredBy] }); } this.emit('todo:sync-updated', { todoId, updateEvent }); } catch (error) { this.logger.error('Error updating todo in sync queue', { todoId, error }); this.emit('sync:error', { type: 'todo-sync-update', todoId, error }); } } this.logger.debug(`Processed ${updates.length} todo updates from sync queue`); } finally { this.processingQueue = false; } } /** * Utility methods for mapping between todo and task formats */ private mapTaskStatusToTodo(taskStatus: TaskStatus): 'pending' | 'in_progress' | 'completed' { switch (taskStatus) { case 'pending': case 'queued': return 'pending'; case 'running': return 'in_progress'; case 'completed': return 'completed'; case 'failed': case 'cancelled': return 'pending'; // Reset failed/cancelled tasks to pending default: return 'pending'; } } private mapTaskPriorityToTodo(priority: number): 'high' | 'medium' | 'low' | 'critical' { if (priority >= 90) return 'critical'; if (priority >= 70) return 'high'; if (priority >= 40) return 'medium'; return 'low'; } private mapTodoPriorityToTask(priority: 'high' | 'medium' | 'low' | 'critical'): number { switch (priority) { case 'critical': return 95; case 'high': return 80; case 'medium': return 50; case 'low': return 20; default: return 50; } } private inferTaskTypeFromTodo(todo: TodoItem): string { const content = todo.content.toLowerCase(); if (content.includes('test') || content.includes('spec')) return 'testing'; if (content.includes('deploy') || content.includes('release')) return 'deployment'; if (content.includes('research') || content.includes('analyze')) return 'research'; if (content.includes('implement') || content.includes('code') || content.includes('build')) return 'development'; if (content.includes('design') || content.includes('architect')) return 'design'; if (content.includes('document') || content.includes('doc')) return 'documentation'; return 'general'; } private parseEstimatedTime(timeStr?: string): number | undefined { if (!timeStr) return undefined; const match = timeStr.match(/(\d+)\s*(min|hour|h|day|d)/i); if (!match) return undefined; const value = parseInt(match[1]); const unit = match[2].toLowerCase(); switch (unit) { case 'min': return value * 60 * 1000; case 'hour': case 'h': return value * 60 * 60 * 1000; case 'day': case 'd': return value * 24 * 60 * 60 * 1000; default: return undefined; } } /** * Mapping management methods */ private addMapping(taskId: string, mapping: TodoTaskMapping): void { const mappings = this.mappings.get(taskId) || []; mappings.push(mapping); this.mappings.set(taskId, mappings); } private findMappingsByTodoId(todoId: string): TodoTaskMapping[] { const allMappings: TodoTaskMapping[] = []; for (const mappings of this.mappings.values()) { allMappings.push(...mappings.filter(m => m.todoId === todoId)); } return allMappings; } /** * Start the sync timer for periodic operations */ private startSyncTimer(): void { if (this.config.syncInterval && this.config.syncInterval > 0) { this.syncTimer = setInterval(() => { this.processSyncQueue(); }, this.config.syncInterval); } } /** * Public API methods */ /** * Manually create a mapping between a todo and task */ async createMapping(todoId: string, taskId: string, relationship: 'one-to-one' | 'parent-child' | 'todo-breakdown' = 'one-to-one'): Promise { const mapping: TodoTaskMapping = { todoId, taskId, relationship, createdAt: new Date(), metadata: { manuallyCreated: true } }; this.addMapping(taskId, mapping); if (this.memoryManager) { await this.memoryManager.store(`mapping:${taskId}`, mapping, { namespace: this.config.memoryNamespace, tags: ['mapping', 'manual'] }); } this.logger.info(`Created manual mapping between todo ${todoId} and task ${taskId}`); this.emit('mapping:created', { mapping }); } /** * Get all mappings for a task or todo */ getMappings(id: string, type: 'task' | 'todo' = 'task'): TodoTaskMapping[] { if (type === 'task') { return this.mappings.get(id) || []; } else { return this.findMappingsByTodoId(id); } } /** * Get sync statistics */ getSyncStats(): { totalMappings: number; queuedUpdates: number; processingQueue: boolean; configuration: Partial; } { const totalMappings = Array.from(this.mappings.values()).reduce((sum, mappings) => sum + mappings.length, 0); return { totalMappings, queuedUpdates: this.syncQueue.size, processingQueue: this.processingQueue, configuration: this.config }; } /** * Cleanup and shutdown */ async shutdown(): Promise { if (this.syncTimer) { clearInterval(this.syncTimer); this.syncTimer = undefined; } // Process any remaining updates if (this.syncQueue.size > 0) { await this.processSyncQueue(); } this.removeAllListeners(); this.logger.info('TodoSyncService shutdown complete'); } }