UNPKG

devcontext

Version:

DevContext is a cutting-edge Model Context Protocol (MCP) server designed to provide developers with continuous, project-centric context awareness.

555 lines (486 loc) 16.9 kB
/** * TimelineManagerLogic.js * * Provides functions for managing and recording timeline events. */ import { v4 as uuidv4 } from "uuid"; import { executeQuery } from "../db.js"; /** * @typedef {Object} TimelineEvent * @property {string} event_id - Unique identifier for the event * @property {string} event_type - Type of event * @property {number} timestamp - Timestamp when the event occurred * @property {Object} data - Event data parsed from JSON * @property {string[]} associated_entity_ids - IDs of entities associated with this event * @property {string|null} conversation_id - Optional conversation ID this event belongs to * @property {string} created_at - Timestamp when the event was created in the database */ /** * @typedef {Object} Snapshot * @property {string} snapshot_id - Unique identifier for the snapshot * @property {string|null} name - Optional name of the snapshot * @property {string|null} description - Optional description of the snapshot * @property {Object} snapshot_data - Parsed snapshot data from JSON * @property {string|null} timeline_event_id - Optional ID of the associated timeline event * @property {string} created_at - Timestamp when the snapshot was created in the database */ /** * Records an event in the timeline * * @param {string} type - Type of event * @param {object} data - Event data object * @param {string[]} [associatedEntityIds=[]] - IDs of entities associated with this event * @param {string} [conversationId] - Optional conversation ID this event belongs to * @returns {Promise<string>} Generated event ID */ export async function recordEvent( type, data, associatedEntityIds = [], conversationId = null ) { try { // Generate a unique event ID const eventId = uuidv4(); // Convert data and associatedEntityIds to JSON strings const dataJson = JSON.stringify(data); const entityIdsJson = JSON.stringify(associatedEntityIds); // Get current timestamp const timestamp = Date.now(); // Construct the SQL query const query = ` INSERT INTO timeline_events ( event_id, event_type, timestamp, data, associated_entity_ids, conversation_id ) VALUES (?, ?, ?, ?, ?, ?) `; // Execute the query with parameters await executeQuery(query, [ eventId, type, timestamp, dataJson, entityIdsJson, conversationId, ]); return eventId; } catch (error) { console.error(`Error recording timeline event (${type}):`, error); throw error; } } /** * Creates a snapshot of the active context data * * @param {object} activeContextData - Data to be snapshotted (active entity IDs, focus, etc.) * @param {string} [name] - Optional name for the snapshot * @param {string} [description] - Optional description of the snapshot * @param {string} [timeline_event_id] - Optional ID of an associated timeline event * @returns {Promise<string>} Generated snapshot ID */ export async function createSnapshot( activeContextData, name = null, description = null, timeline_event_id = null ) { try { // Generate a unique snapshot ID const snapshot_id = uuidv4(); // Convert activeContextData to a JSON string const snapshot_data = JSON.stringify(activeContextData); // Construct the SQL query const query = ` INSERT INTO context_snapshots ( snapshot_id, name, description, snapshot_data, timeline_event_id ) VALUES (?, ?, ?, ?, ?) `; // Execute the query with parameters await executeQuery(query, [ snapshot_id, name, description, snapshot_data, timeline_event_id, ]); return snapshot_id; } catch (error) { console.error("Error creating context snapshot:", error); throw error; } } /** * Manages the creation of implicit checkpoints based on activity thresholds * This function checks for substantial activity and creates automatic snapshots when appropriate * * @returns {Promise<void>} */ export async function manageImplicitCheckpoints() { try { // Define activity thresholds const MIN_EVENTS_FOR_CHECKPOINT = 10; const MIN_MINUTES_SINCE_LAST_CHECKPOINT = 15; const SIGNIFICANT_EVENT_TYPES = [ "code_change", "conversation_end", "focus_change", ]; // Get the timestamp of the last implicit checkpoint const lastCheckpointQuery = ` SELECT cs.snapshot_id, te.timestamp FROM context_snapshots cs LEFT JOIN timeline_events te ON cs.timeline_event_id = te.event_id WHERE (cs.name LIKE 'Implicit Checkpoint%' OR te.event_type = 'implicit_checkpoint_creation') ORDER BY te.timestamp DESC LIMIT 1 `; const lastCheckpoint = await executeQuery(lastCheckpointQuery); const lastCheckpointTime = lastCheckpoint.rows && lastCheckpoint.rows.length > 0 ? lastCheckpoint.rows[0].timestamp : 0; // Calculate time threshold const timeThreshold = Date.now() - MIN_MINUTES_SINCE_LAST_CHECKPOINT * 60 * 1000; // Check if enough time has passed since last checkpoint if (lastCheckpointTime > timeThreshold) { // Not enough time has passed return; } // Count events since last checkpoint const countEventsQuery = ` SELECT COUNT(*) as event_count FROM timeline_events WHERE timestamp > ? `; const eventCountResult = await executeQuery(countEventsQuery, [ lastCheckpointTime, ]); const eventCount = eventCountResult.rows && eventCountResult.rows.length > 0 ? eventCountResult.rows[0].event_count || 0 : 0; // Count significant events const significantEventsQuery = ` SELECT COUNT(*) as significant_count FROM timeline_events WHERE timestamp > ? AND event_type IN (${SIGNIFICANT_EVENT_TYPES.map( () => "?" ).join(",")}) `; const significantCountResult = await executeQuery(significantEventsQuery, [ lastCheckpointTime, ...SIGNIFICANT_EVENT_TYPES, ]); const significantCount = significantCountResult.rows && significantCountResult.rows.length > 0 ? significantCountResult.rows[0].significant_count || 0 : 0; // Determine if we should create a checkpoint const shouldCreateCheckpoint = eventCount >= MIN_EVENTS_FOR_CHECKPOINT || significantCount > 0; if (shouldCreateCheckpoint) { // Get active context data // In a real implementation, this would come from ActiveContextManager // Since that's not available, we'll create mock data to demonstrate the function const activeContextData = { activeEntities: [], // This would be populated with actual entity IDs activeFocus: null, // This would be the current focus area timestamp: Date.now(), }; // Try to get actual context data if available try { // This is a placeholder - in real implementation, we would check if // ActiveContextManager is available and use it to get context data const ActiveContextManager = global.ActiveContextManager; if ( ActiveContextManager && typeof ActiveContextManager.getActiveContextAsEntities === "function" ) { const contextData = await ActiveContextManager.getActiveContextAsEntities(); if (contextData) { activeContextData.activeEntities = contextData.entities || []; activeContextData.activeFocus = contextData.focus || null; } } } catch (error) { console.warn( "Could not retrieve data from ActiveContextManager:", error.message ); // Continue with mock data } // Generate checkpoint name and description const timestamp = new Date().toISOString(); const checkpointName = `Implicit Checkpoint [${timestamp}]`; let description = "Automatically created checkpoint due to "; if (eventCount >= MIN_EVENTS_FOR_CHECKPOINT) { description += `high activity (${eventCount} events)`; } else if (significantCount > 0) { description += `significant changes (${significantCount} significant events)`; } // Record the checkpoint creation event const eventId = await recordEvent("implicit_checkpoint_creation", { reason: description, eventCount, significantCount, }); // Create the snapshot await createSnapshot( activeContextData, checkpointName, description, eventId ); } } catch (error) { console.error("Error managing implicit checkpoints:", error); // Don't throw - this function should not crash the application } } /** * Retrieves timeline events based on specified filters * * @param {Object} options - Query options * @param {string[]} [options.types] - Filter events by these event types * @param {number} [options.limit] - Maximum number of events to return * @param {string} [options.conversationId] - Filter events by this conversation ID * @param {boolean} [options.includeMilestones=true] - Whether to include milestone events * @param {string} [options.excludeConversationId] - Exclude events with this conversation ID * @returns {Promise<TimelineEvent[]>} Array of timeline events with parsed JSON fields */ export async function getEvents(options = {}) { try { const { types, limit, conversationId, includeMilestones = true, excludeConversationId, } = options; // Build the base query let query = "SELECT * FROM timeline_events WHERE 1=1"; const params = []; // Apply filters based on options if (types && types.length > 0) { query += ` AND event_type IN (${types.map(() => "?").join(",")})`; params.push(...types); } if (conversationId) { query += " AND conversation_id = ?"; params.push(conversationId); } if (excludeConversationId) { query += " AND (conversation_id != ? OR conversation_id IS NULL)"; params.push(excludeConversationId); } // Handle milestone events filtering // Assuming milestone events have specific types like 'milestone_created' or are linked to snapshots if (!includeMilestones) { // Define the event types that are considered milestones const milestoneEventTypes = [ "milestone_created", "implicit_checkpoint_creation", "checkpoint_created", ]; query += ` AND event_type NOT IN (${milestoneEventTypes .map(() => "?") .join(",")})`; params.push(...milestoneEventTypes); // Additionally exclude events that have an associated snapshot query += ` AND NOT EXISTS ( SELECT 1 FROM context_snapshots WHERE context_snapshots.timeline_event_id = timeline_events.event_id )`; } // Add ordering query += " ORDER BY timestamp DESC"; // Apply limit if specified if (limit && Number.isInteger(limit) && limit > 0) { query += " LIMIT ?"; params.push(limit); } // Execute the query const events = await executeQuery(query, params); // Check if events has a rows property and it's an array const rows = events && events.rows && Array.isArray(events.rows) ? events.rows : Array.isArray(events) ? events : []; // If no valid results, return empty array if (rows.length === 0) { console.warn("No valid timeline events found"); return []; } // Parse JSON fields in each event return rows.map((event) => ({ ...event, data: JSON.parse(event.data || "{}"), associated_entity_ids: JSON.parse(event.associated_entity_ids || "[]"), })); } catch (error) { console.error("Error retrieving timeline events:", error); throw error; } } /** * Retrieves context snapshots (milestones) based on specified filters * * @param {Object} options - Query options * @param {string[]} [options.types] - Filter snapshots by type-related keywords in name or description * @param {number} [options.limit] - Maximum number of snapshots to return * @returns {Promise<Snapshot[]>} Array of context snapshots with parsed snapshot_data field */ export async function getMilestones(options = {}) { try { const { types, limit } = options; // Start building the query let query = ` SELECT cs.*, te.event_type FROM context_snapshots cs LEFT JOIN timeline_events te ON cs.timeline_event_id = te.event_id WHERE 1=1 `; const params = []; // Apply type filtering based on name, description or associated event type if (types && types.length > 0) { const typeConditions = []; for (const type of types) { // Create pattern for LIKE queries const pattern = `%${type}%`; // Add conditions for name, description and associated event type typeConditions.push("cs.name LIKE ?"); params.push(pattern); typeConditions.push("cs.description LIKE ?"); params.push(pattern); typeConditions.push("te.event_type LIKE ?"); params.push(pattern); // Also search in event data if linked to a timeline event typeConditions.push(` EXISTS ( SELECT 1 FROM timeline_events WHERE timeline_events.event_id = cs.timeline_event_id AND timeline_events.data LIKE ? ) `); params.push(`%"category":"${type}"%`); } if (typeConditions.length > 0) { query += ` AND (${typeConditions.join(" OR ")})`; } } // Add ordering by timestamp (assuming cs.timestamp exists) query += " ORDER BY timestamp DESC"; // Apply limit if specified if (limit && Number.isInteger(limit) && limit > 0) { query += " LIMIT ?"; params.push(limit); } // Execute the query const snapshots = await executeQuery(query, params); // Check if snapshots has a rows property and it's an array const rows = snapshots && snapshots.rows && Array.isArray(snapshots.rows) ? snapshots.rows : Array.isArray(snapshots) ? snapshots : []; // If no valid results, return empty array if (rows.length === 0) { console.warn("No valid snapshots found"); return []; } // Parse snapshot_data from JSON for each result return rows.map((snapshot) => ({ ...snapshot, snapshot_data: JSON.parse(snapshot.snapshot_data || "{}"), })); } catch (error) { console.error("Error retrieving milestones:", error); throw error; } } /** * Gets recent events for a specific conversation * * @param {string} conversationId - The conversation ID * @param {number} [limit=10] - Maximum number of events to return * @param {string[]} [eventTypes] - Optional array of event types to filter by * @returns {Promise<TimelineEvent[]>} Array of timeline events */ export async function getRecentEventsForConversation( conversationId, limit = 10, eventTypes = null ) { try { if (!conversationId) { throw new Error("Conversation ID is required"); } // Build the query let query = ` SELECT event_id, event_type, timestamp, data, associated_entity_ids, conversation_id FROM timeline_events WHERE conversation_id = ? `; const params = [conversationId]; // Add event type filter if provided if (eventTypes && Array.isArray(eventTypes) && eventTypes.length > 0) { const placeholders = eventTypes.map(() => "?").join(","); query += ` AND event_type IN (${placeholders})`; params.push(...eventTypes); } // Add order and limit query += ` ORDER BY timestamp DESC LIMIT ? `; params.push(limit); // Execute the query const results = await executeQuery(query, params); // Check if results has a rows property and it's an array const rows = results && results.rows && Array.isArray(results.rows) ? results.rows : Array.isArray(results) ? results : []; // If no valid results, return empty array if (rows.length === 0) { console.warn("No recent events found for conversation:", conversationId); return []; } // Parse the JSON fields return rows.map((event) => ({ ...event, data: JSON.parse(event.data || "{}"), associated_entity_ids: JSON.parse(event.associated_entity_ids || "[]"), })); } catch (error) { console.error( `Error getting recent events for conversation ${conversationId}:`, error ); return []; } }