// Semantic search operations import { query, queryOne, getProjectKey } from '../db.js'; import { getEmbedding, formatEmbedding } from '../embeddings.js'; import type { SimilarTask } from '../types.js'; interface SessionNote { note_type: string; content: string; created_at: string; } interface SessionTask { id: string; title: string; status: string; priority: string; } interface SessionCommit { commit_hash: string; commit_message: string; } interface TaskSessionContextArgs { id: string; } /** * Get session context for a task - retrieves notes, decisions, and related tasks * from the session where the task was created */ export async function taskSessionContext(args: TaskSessionContextArgs): Promise { const { id } = args; // Get task with session info const task = await queryOne<{ id: string; title: string; description: string; session_id: string; }>( `SELECT t.id, t.title, t.description, t.session_id FROM tasks t WHERE t.id = $1`, [id] ); if (!task) { return `Task not found: ${id}`; } if (!task.session_id) { return `# Context for ${id}\n\n**Task:** ${task.title}\n\n⚠️ No session linked to this task. Task was created before session tracking was implemented or via direct database insert.\n\n${task.description ? `**Description:**\n${task.description}` : ''}`; } // Get session info const session = await queryOne<{ session_number: number; summary: string; started_at: string; }>( `SELECT session_number, summary, to_char(started_at, 'YYYY-MM-DD HH24:MI') as started_at FROM sessions WHERE id = $1`, [task.session_id] ); let output = `# Context for ${id}\n\n`; output += `**Task:** ${task.title}\n`; if (session) { output += `**Created in Session:** #${session.session_number} (${session.started_at})\n`; if (session.summary) { output += `\n## Session Summary\n${session.summary}\n`; } } else { output += `**Session ID:** ${task.session_id} (session record not found)\n`; } if (task.description) { output += `\n## Task Description\n${task.description}\n`; } // Get session notes const notes = await query( `SELECT note_type, content, to_char(created_at, 'HH24:MI') as created_at FROM session_notes WHERE session_id = $1 ORDER BY created_at`, [task.session_id] ); if (notes.length > 0) { output += `\n## Session Notes\n`; for (const note of notes) { output += `- **[${note.note_type}]** ${note.content}\n`; } } // Get related tasks from same session const relatedTasks = await query( `SELECT id, title, status, priority FROM tasks WHERE session_id = $1 AND id != $2 ORDER BY created_at`, [task.session_id, id] ); if (relatedTasks.length > 0) { output += `\n## Other Tasks from Same Session\n`; for (const t of relatedTasks) { const statusIcon = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '▶' : '○'; output += `- ${statusIcon} [${t.priority}] ${t.id}: ${t.title}\n`; } } // Get commits from session const commits = await query( `SELECT DISTINCT commit_hash, commit_message FROM task_commits WHERE task_id IN (SELECT id FROM tasks WHERE session_id = $1) ORDER BY committed_at DESC LIMIT 10`, [task.session_id] ); if (commits.length > 0) { output += `\n## Commits from Session\n`; for (const c of commits) { output += `- \`${c.commit_hash}\` ${c.commit_message}\n`; } } return output; } interface TaskSimilarArgs { query: string; project?: string; limit?: number; } interface TaskContextArgs { description: string; project?: string; limit?: number; } /** * Find semantically similar tasks using pgvector */ export async function taskSimilar(args: TaskSimilarArgs): Promise { const { query: searchQuery, project, limit = 5 } = args; // Generate embedding for the query const embedding = await getEmbedding(searchQuery); if (!embedding) { return 'Error: Could not generate embedding for search query'; } const embeddingStr = formatEmbedding(embedding); let whereClause = 'WHERE embedding IS NOT NULL'; const params: unknown[] = [embeddingStr, limit]; let paramIndex = 3; if (project) { const projectKey = await getProjectKey(project); whereClause += ` AND project = $${paramIndex}`; params.push(projectKey); } const results = await query( `SELECT id, title, type, status, priority, 1 - (embedding <=> $1) as similarity FROM tasks ${whereClause} ORDER BY embedding <=> $1 LIMIT $2`, params ); if (results.length === 0) { return 'No similar tasks found'; } const lines = results.map(t => { const pct = Math.round(t.similarity * 100); const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : '[ ]'; return `${statusIcon} ${pct}% ${t.id}: ${t.title} [${t.type}] [${t.priority}]`; }); return `Similar tasks for "${searchQuery}":\n\n${lines.join('\n')}`; } /** * Get related tasks for current work context * Returns markdown suitable for injection into delegations */ export async function taskContext(args: TaskContextArgs): Promise { const { description, project, limit = 3 } = args; // Generate embedding for the description const embedding = await getEmbedding(description); if (!embedding) { return ''; } const embeddingStr = formatEmbedding(embedding); let whereClause = 'WHERE embedding IS NOT NULL AND status != \'completed\''; const params: unknown[] = [embeddingStr, limit]; let paramIndex = 3; if (project) { const projectKey = await getProjectKey(project); whereClause += ` AND project = $${paramIndex}`; params.push(projectKey); } const results = await query( `SELECT id, title, type, status, priority, 1 - (embedding <=> $1) as similarity FROM tasks ${whereClause} ORDER BY embedding <=> $1 LIMIT $2`, params ); if (results.length === 0) { return ''; } // Format as markdown for delegation context let output = '## Related Tasks\n\n'; for (const t of results) { const pct = Math.round(t.similarity * 100); output += `- **${t.id}**: ${t.title} (${pct}% match, ${t.priority}, ${t.status})\n`; } return output; }