Add session management MCP tools (Phase 3)

Implemented 8 new session management tools:
- session_start: Initialize session with metadata tracking
- session_update: Update session metrics during execution
- session_end: Close session with summary and embedding
- session_list: List sessions with filtering
- session_search: Semantic search across sessions
- session_context: Get complete session context (tasks, commits, builds, memories)
- build_record: Link builds to sessions and versions
- session_commit_link: Link commits to sessions

Enhanced existing tools:
- memory_add: Added session_id and task_id parameters
- Updated all memory queries to use 'memories' table (renamed from session_memories)

Implementation:
- Created src/tools/sessions.ts with all session operations
- Updated src/tools/memories.ts for new schema
- Added 8 session tool definitions to src/tools/index.ts
- Registered all session tools in src/index.ts switch statement
- TypeScript compilation successful

Related: CF-167 (Fix shared session-summary.md file conflict)
This commit is contained in:
Christian Gick
2026-01-17 07:45:14 +02:00
parent 00de7f1299
commit 04395e8403
5 changed files with 640 additions and 27 deletions

418
src/tools/sessions.ts Normal file
View File

@@ -0,0 +1,418 @@
// Session management operations for database-driven session tracking
import { query, queryOne, execute } from '../db.js';
import { getEmbedding, formatEmbedding } from '../embeddings.js';
interface SessionStartArgs {
session_id?: string;
project: string;
working_directory?: string;
git_branch?: string;
initial_prompt?: string;
}
interface SessionUpdateArgs {
session_id: string;
message_count?: number;
token_count?: number;
tools_used?: string[];
}
interface SessionEndArgs {
session_id: string;
summary: string;
status?: 'completed' | 'interrupted';
}
interface SessionListArgs {
project?: string;
status?: 'active' | 'completed' | 'interrupted';
since?: string;
limit?: number;
}
interface SessionSearchArgs {
query: string;
project?: string;
limit?: number;
}
interface Session {
id: string;
project: string | null;
session_number: number | null;
started_at: string;
ended_at: string | null;
duration_minutes: number | null;
working_directory: string | null;
git_branch: string | null;
initial_prompt: string | null;
summary: string | null;
message_count: number;
token_count: number;
tools_used: string[] | null;
status: string;
created_at: string;
}
/**
* Start a new session with metadata tracking
* Returns session_id and session_number
*/
export async function sessionStart(args: SessionStartArgs): Promise<string> {
const { session_id, project, working_directory, git_branch, initial_prompt } = args;
// Generate session ID if not provided (fallback, should come from session-memory)
const id = session_id || `session_${Date.now()}_${Math.random().toString(36).substring(7)}`;
await execute(
`INSERT INTO sessions (id, project, started_at, working_directory, git_branch, initial_prompt, status)
VALUES ($1, $2, NOW(), $3, $4, $5, 'active')`,
[id, project, working_directory || null, git_branch || null, initial_prompt || null]
);
// Get the assigned session_number
const result = await queryOne<{ session_number: number }>(
'SELECT session_number FROM sessions WHERE id = $1',
[id]
);
const session_number = result?.session_number || null;
return `Session started: ${id} (${project} #${session_number})`;
}
/**
* Update session metadata during execution
*/
export async function sessionUpdate(args: SessionUpdateArgs): Promise<string> {
const { session_id, message_count, token_count, tools_used } = args;
const updates: string[] = [];
const params: unknown[] = [];
let paramIndex = 1;
if (message_count !== undefined) {
updates.push(`message_count = $${paramIndex++}`);
params.push(message_count);
}
if (token_count !== undefined) {
updates.push(`token_count = $${paramIndex++}`);
params.push(token_count);
}
if (tools_used !== undefined) {
updates.push(`tools_used = $${paramIndex++}`);
params.push(tools_used);
}
if (updates.length === 0) {
return 'No updates provided';
}
updates.push(`updated_at = NOW()`);
params.push(session_id);
await execute(
`UPDATE sessions SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
params
);
return `Session updated: ${session_id}`;
}
/**
* End session and generate summary with embedding
*/
export async function sessionEnd(args: SessionEndArgs): Promise<string> {
const { session_id, summary, status = 'completed' } = args;
// Generate embedding for semantic search
const embedding = await getEmbedding(summary);
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
if (embeddingValue) {
await execute(
`UPDATE sessions
SET ended_at = NOW(),
summary = $1,
embedding = $2,
status = $3,
updated_at = NOW()
WHERE id = $4`,
[summary, embeddingValue, status, session_id]
);
} else {
await execute(
`UPDATE sessions
SET ended_at = NOW(),
summary = $1,
status = $2,
updated_at = NOW()
WHERE id = $3`,
[summary, status, session_id]
);
}
// Get session details
const session = await queryOne<Session>(
`SELECT id, project, session_number, duration_minutes
FROM sessions WHERE id = $1`,
[session_id]
);
if (!session) {
return `Session ended: ${session_id}`;
}
return `Session ended: ${session.project} #${session.session_number} (${session.duration_minutes || 0}m)`;
}
/**
* List sessions with filtering and pagination
*/
export async function sessionList(args: SessionListArgs): Promise<string> {
const { project, status, since, limit = 20 } = args;
let whereClause = 'WHERE 1=1';
const params: unknown[] = [];
let paramIndex = 1;
if (project) {
whereClause += ` AND project = $${paramIndex++}`;
params.push(project);
}
if (status) {
whereClause += ` AND status = $${paramIndex++}`;
params.push(status);
}
if (since) {
whereClause += ` AND started_at >= $${paramIndex++}::timestamp`;
params.push(since);
}
params.push(limit);
const sessions = await query<Session>(
`SELECT id, project, session_number, started_at, ended_at, duration_minutes,
summary, message_count, token_count, status
FROM sessions
${whereClause}
ORDER BY started_at DESC
LIMIT $${paramIndex}`,
params
);
if (sessions.length === 0) {
return `No sessions found${project ? ` for project ${project}` : ''}`;
}
const lines: string[] = [];
for (const s of sessions) {
const num = s.session_number ? `#${s.session_number}` : '';
const duration = s.duration_minutes ? `${s.duration_minutes}m` : 'active';
const messages = s.message_count ? `${s.message_count} msgs` : '';
const summaryPreview = s.summary ? s.summary.slice(0, 60) + '...' : 'No summary';
lines.push(`${s.project} ${num} (${duration}, ${messages}) - ${summaryPreview}`);
}
return `Sessions:\n${lines.join('\n')}`;
}
/**
* Semantic search across sessions using vector similarity
*/
export async function sessionSearch(args: SessionSearchArgs): Promise<string> {
const { query: searchQuery, project, limit = 5 } = args;
// Generate embedding for search
const embedding = await getEmbedding(searchQuery);
if (!embedding) {
return 'Error: Could not generate embedding for search';
}
const embeddingStr = formatEmbedding(embedding);
let whereClause = 'WHERE embedding IS NOT NULL';
const params: unknown[] = [embeddingStr, limit];
if (project) {
whereClause += ` AND project = $3`;
params.splice(1, 0, project); // Insert before limit
params[2] = limit; // Adjust limit position
}
const sessions = await query<Session & { similarity: number }>(
`SELECT id, project, session_number, started_at, duration_minutes, summary,
1 - (embedding <=> $1) as similarity
FROM sessions
${whereClause}
ORDER BY embedding <=> $1
LIMIT $${project ? '3' : '2'}`,
params
);
if (sessions.length === 0) {
return 'No relevant sessions found';
}
const lines = ['Similar sessions:\n'];
for (const s of sessions) {
const sim = Math.round(s.similarity * 100);
const num = s.session_number ? `#${s.session_number}` : '';
const duration = s.duration_minutes ? `(${s.duration_minutes}m)` : '';
lines.push(`**${s.project} ${num}** ${duration} (${sim}% match)`);
lines.push(` ${s.summary || 'No summary'}`);
lines.push('');
}
return lines.join('\n');
}
/**
* Get complete session context: tasks, commits, builds, memories
*/
export async function sessionContext(session_id: string): Promise<string> {
// Get session details
const session = await queryOne<Session>(
`SELECT * FROM sessions WHERE id = $1`,
[session_id]
);
if (!session) {
return `Session not found: ${session_id}`;
}
const lines: string[] = [];
lines.push(`**Session: ${session.project} #${session.session_number}**`);
lines.push(`Started: ${session.started_at}`);
if (session.ended_at) {
lines.push(`Ended: ${session.ended_at} (${session.duration_minutes}m)`);
}
if (session.summary) {
lines.push(`\nSummary: ${session.summary}`);
}
lines.push('');
// Get tasks touched in this session
const tasks = await query<{ task_id: string; title: string; status: string; activity_count: number }>(
`SELECT task_id, title, status, activity_count
FROM session_tasks
WHERE session_id = $1
ORDER BY first_touched`,
[session_id]
);
if (tasks.length > 0) {
lines.push(`**Tasks (${tasks.length}):**`);
for (const t of tasks) {
lines.push(`${t.task_id}: ${t.title} [${t.status}] (${t.activity_count} activities)`);
}
lines.push('');
}
// Get commits made in this session
const commits = await query<{ commit_sha: string; repo: string; commit_message: string | null }>(
`SELECT commit_sha, repo, commit_message
FROM session_commits
WHERE session_id = $1
ORDER BY committed_at DESC`,
[session_id]
);
if (commits.length > 0) {
lines.push(`**Commits (${commits.length}):**`);
for (const c of commits) {
const msg = c.commit_message ? c.commit_message.split('\n')[0] : 'No message';
lines.push(`${c.commit_sha.substring(0, 7)} (${c.repo}): ${msg}`);
}
lines.push('');
}
// Get builds linked to this session
const builds = await query<{ build_number: number; status: string; version_id: string | null }>(
`SELECT build_number, status, version_id
FROM builds
WHERE session_id = $1
ORDER BY started_at DESC`,
[session_id]
);
if (builds.length > 0) {
lines.push(`**Builds (${builds.length}):**`);
for (const b of builds) {
lines.push(`• Build #${b.build_number}: ${b.status}${b.version_id ? ` (${b.version_id})` : ''}`);
}
lines.push('');
}
// Get memories stored in this session
const memories = await query<{ category: string; title: string; content: string }>(
`SELECT category, title, content
FROM memories
WHERE session_id = $1
ORDER BY created_at`,
[session_id]
);
if (memories.length > 0) {
lines.push(`**Memories (${memories.length}):**`);
for (const m of memories) {
lines.push(`• [${m.category}] ${m.title}`);
lines.push(` ${m.content.slice(0, 100)}${m.content.length > 100 ? '...' : ''}`);
}
}
// Show metrics
if (session.message_count || session.token_count || session.tools_used) {
lines.push('\n**Metrics:**');
if (session.message_count) lines.push(`• Messages: ${session.message_count}`);
if (session.token_count) lines.push(`• Tokens: ${session.token_count}`);
if (session.tools_used && session.tools_used.length > 0) {
lines.push(`• Tools: ${session.tools_used.join(', ')}`);
}
}
return lines.join('\n');
}
/**
* Record build information linked to session and version
*/
export async function buildRecord(
session_id: string | null,
version_id: string,
build_number: number,
git_commit_sha: string | null,
status: string,
started_at: string
): Promise<string> {
await execute(
`INSERT INTO builds (session_id, version_id, build_number, git_commit_sha, status, started_at)
VALUES ($1, $2, $3, $4, $5, $6)`,
[session_id, version_id, build_number, git_commit_sha, status, started_at]
);
return `Build recorded: #${build_number} for ${version_id} (${status})`;
}
/**
* Link a commit to a session (automatically called when commits are made)
*/
export async function sessionCommitLink(
session_id: string,
commit_sha: string,
repo: string,
commit_message: string | null,
committed_at: string | null
): Promise<string> {
await execute(
`INSERT INTO session_commits (session_id, commit_sha, repo, commit_message, committed_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (session_id, commit_sha) DO NOTHING`,
[session_id, commit_sha, repo, commit_message, committed_at]
);
return `Linked commit ${commit_sha.substring(0, 7)} to session ${session_id}`;
}