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:
@@ -4,9 +4,9 @@ Exposes task management tools via Model Context Protocol. Uses PostgreSQL with p
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- SSH tunnel to docker-host: `ssh -L 5432:localhost:5432 docker-host -N &`
|
- SSH tunnel to services: `ssh -L 5432:localhost:5432 services -N &`
|
||||||
- PostgreSQL with pgvector on docker-host (CI stack `postgres` container)
|
- PostgreSQL with pgvector on services (CI stack `postgres` container)
|
||||||
- CI postgres must be running: `ssh docker-host "cd /opt/docker/ci && docker compose up -d postgres"`
|
- CI postgres must be running: `ssh services "cd /opt/docker/ci && docker compose up -d postgres"`
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
75
src/index.ts
75
src/index.ts
@@ -37,6 +37,16 @@ import {
|
|||||||
componentGraph,
|
componentGraph,
|
||||||
} from './tools/impact.js';
|
} from './tools/impact.js';
|
||||||
import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js';
|
import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js';
|
||||||
|
import {
|
||||||
|
sessionStart,
|
||||||
|
sessionUpdate,
|
||||||
|
sessionEnd,
|
||||||
|
sessionList,
|
||||||
|
sessionSearch,
|
||||||
|
sessionContext,
|
||||||
|
buildRecord,
|
||||||
|
sessionCommitLink,
|
||||||
|
} from './tools/sessions.js';
|
||||||
|
|
||||||
// Create MCP server
|
// Create MCP server
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
@@ -329,6 +339,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
content: a.content,
|
content: a.content,
|
||||||
context: a.context,
|
context: a.context,
|
||||||
project: a.project,
|
project: a.project,
|
||||||
|
session_id: a.session_id,
|
||||||
|
task_id: a.task_id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'memory_search':
|
case 'memory_search':
|
||||||
@@ -350,6 +362,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
result = await memoryContext(a.project, a.task_description);
|
result = await memoryContext(a.project, a.task_description);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
case 'session_start':
|
||||||
|
result = await sessionStart({
|
||||||
|
session_id: a.session_id,
|
||||||
|
project: a.project,
|
||||||
|
working_directory: a.working_directory,
|
||||||
|
git_branch: a.git_branch,
|
||||||
|
initial_prompt: a.initial_prompt,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_update':
|
||||||
|
result = await sessionUpdate({
|
||||||
|
session_id: a.session_id,
|
||||||
|
message_count: a.message_count,
|
||||||
|
token_count: a.token_count,
|
||||||
|
tools_used: a.tools_used,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_end':
|
||||||
|
result = await sessionEnd({
|
||||||
|
session_id: a.session_id,
|
||||||
|
summary: a.summary,
|
||||||
|
status: a.status,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_list':
|
||||||
|
result = await sessionList({
|
||||||
|
project: a.project,
|
||||||
|
status: a.status,
|
||||||
|
since: a.since,
|
||||||
|
limit: a.limit,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_search':
|
||||||
|
result = await sessionSearch({
|
||||||
|
query: a.query,
|
||||||
|
project: a.project,
|
||||||
|
limit: a.limit,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_context':
|
||||||
|
result = await sessionContext(a.session_id);
|
||||||
|
break;
|
||||||
|
case 'build_record':
|
||||||
|
result = await buildRecord(
|
||||||
|
a.session_id,
|
||||||
|
a.version_id,
|
||||||
|
a.build_number,
|
||||||
|
a.git_commit_sha,
|
||||||
|
a.status,
|
||||||
|
a.started_at
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'session_commit_link':
|
||||||
|
result = await sessionCommitLink(
|
||||||
|
a.session_id,
|
||||||
|
a.commit_sha,
|
||||||
|
a.repo,
|
||||||
|
a.commit_message,
|
||||||
|
a.committed_at
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -559,6 +559,8 @@ export const toolDefinitions = [
|
|||||||
content: { type: 'string', description: 'The learning/insight to remember' },
|
content: { type: 'string', description: 'The learning/insight to remember' },
|
||||||
context: { type: 'string', description: 'When/where this applies (optional)' },
|
context: { type: 'string', description: 'When/where this applies (optional)' },
|
||||||
project: { type: 'string', description: 'Project this relates to (optional)' },
|
project: { type: 'string', description: 'Project this relates to (optional)' },
|
||||||
|
session_id: { type: 'string', description: 'Session ID to link memory to (optional)' },
|
||||||
|
task_id: { type: 'string', description: 'Task ID to link memory to (optional)' },
|
||||||
},
|
},
|
||||||
required: ['category', 'title', 'content'],
|
required: ['category', 'title', 'content'],
|
||||||
},
|
},
|
||||||
@@ -600,4 +602,120 @@ export const toolDefinitions = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Session Management Tools
|
||||||
|
{
|
||||||
|
name: 'session_start',
|
||||||
|
description: 'Start a new session with metadata tracking',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID (auto-generated if not provided)' },
|
||||||
|
project: { type: 'string', description: 'Project key (e.g., CF, VPN)' },
|
||||||
|
working_directory: { type: 'string', description: 'Current working directory' },
|
||||||
|
git_branch: { type: 'string', description: 'Current git branch' },
|
||||||
|
initial_prompt: { type: 'string', description: 'First user message' },
|
||||||
|
},
|
||||||
|
required: ['project'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_update',
|
||||||
|
description: 'Update session metrics (message count, tokens, tools used)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID to update' },
|
||||||
|
message_count: { type: 'number', description: 'Number of messages exchanged' },
|
||||||
|
token_count: { type: 'number', description: 'Total tokens used' },
|
||||||
|
tools_used: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Array of tool names used',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['session_id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_end',
|
||||||
|
description: 'End session and generate summary with embedding',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID to end' },
|
||||||
|
summary: { type: 'string', description: 'Session summary text' },
|
||||||
|
status: { type: 'string', enum: ['completed', 'interrupted'], description: 'Session completion status (default: completed)' },
|
||||||
|
},
|
||||||
|
required: ['session_id', 'summary'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_list',
|
||||||
|
description: 'List sessions with filtering and pagination',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
project: { type: 'string', description: 'Filter by project key' },
|
||||||
|
status: { type: 'string', enum: ['active', 'completed', 'interrupted'], description: 'Filter by status' },
|
||||||
|
since: { type: 'string', description: 'Show sessions since date (ISO format)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_search',
|
||||||
|
description: 'Find similar sessions using vector search',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search query' },
|
||||||
|
project: { type: 'string', description: 'Filter by project (optional)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 5)' },
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_context',
|
||||||
|
description: 'Get complete context: tasks, commits, builds, memories',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID to get context for' },
|
||||||
|
},
|
||||||
|
required: ['session_id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'build_record',
|
||||||
|
description: 'Record build information linked to session and version',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID (optional)' },
|
||||||
|
version_id: { type: 'string', description: 'Version ID being built' },
|
||||||
|
build_number: { type: 'number', description: 'Build number' },
|
||||||
|
git_commit_sha: { type: 'string', description: 'Git commit SHA' },
|
||||||
|
status: { type: 'string', description: 'Build status (pending, running, success, failed)' },
|
||||||
|
started_at: { type: 'string', description: 'Build start timestamp (ISO format)' },
|
||||||
|
},
|
||||||
|
required: ['version_id', 'build_number', 'status', 'started_at'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_commit_link',
|
||||||
|
description: 'Link a commit to a session (automatically called when commits are made)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
commit_sha: { type: 'string', description: 'Git commit SHA' },
|
||||||
|
repo: { type: 'string', description: 'Repository (e.g., christian/ClaudeFramework)' },
|
||||||
|
commit_message: { type: 'string', description: 'Commit message (optional)' },
|
||||||
|
committed_at: { type: 'string', description: 'Commit timestamp (ISO format, optional)' },
|
||||||
|
},
|
||||||
|
required: ['session_id', 'commit_sha', 'repo'],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ interface Memory {
|
|||||||
content: string;
|
content: string;
|
||||||
context: string | null;
|
context: string | null;
|
||||||
project: string | null;
|
project: string | null;
|
||||||
source_session: string | null;
|
session_id: string | null;
|
||||||
times_surfaced: number;
|
task_id: string | null;
|
||||||
|
access_count: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ interface MemoryAddArgs {
|
|||||||
content: string;
|
content: string;
|
||||||
context?: string;
|
context?: string;
|
||||||
project?: string;
|
project?: string;
|
||||||
source_session?: string;
|
session_id?: string;
|
||||||
|
task_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MemorySearchArgs {
|
interface MemorySearchArgs {
|
||||||
@@ -40,10 +42,10 @@ interface MemoryListArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new memory/learning
|
* Add a new memory/learning (enhanced with session_id and task_id)
|
||||||
*/
|
*/
|
||||||
export async function memoryAdd(args: MemoryAddArgs): Promise<string> {
|
export async function memoryAdd(args: MemoryAddArgs): Promise<string> {
|
||||||
const { category, title, content, context, project, source_session } = args;
|
const { category, title, content, context, project, session_id, task_id } = args;
|
||||||
|
|
||||||
// Generate embedding for semantic search
|
// Generate embedding for semantic search
|
||||||
const embedText = `${title}. ${content}`;
|
const embedText = `${title}. ${content}`;
|
||||||
@@ -52,15 +54,15 @@ export async function memoryAdd(args: MemoryAddArgs): Promise<string> {
|
|||||||
|
|
||||||
if (embeddingValue) {
|
if (embeddingValue) {
|
||||||
await execute(
|
await execute(
|
||||||
`INSERT INTO session_memories (category, title, content, context, project, source_session, embedding)
|
`INSERT INTO memories (category, title, content, context, project, session_id, task_id, embedding)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
[category, title, content, context || null, project || null, source_session || null, embeddingValue]
|
[category, title, content, context || null, project || null, session_id || null, task_id || null, embeddingValue]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execute(
|
await execute(
|
||||||
`INSERT INTO session_memories (category, title, content, context, project, source_session)
|
`INSERT INTO memories (category, title, content, context, project, session_id, task_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
[category, title, content, context || null, project || null, source_session || null]
|
[category, title, content, context || null, project || null, session_id || null, task_id || null]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +98,10 @@ export async function memorySearch(args: MemorySearchArgs): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const memories = await query<Memory & { similarity: number }>(
|
const memories = await query<Memory & { similarity: number }>(
|
||||||
`SELECT id, category, title, content, context, project, times_surfaced,
|
`SELECT id, category, title, content, context, project, access_count,
|
||||||
to_char(created_at, 'YYYY-MM-DD') as created_at,
|
to_char(created_at, 'YYYY-MM-DD') as created_at,
|
||||||
1 - (embedding <=> $1) as similarity
|
1 - (embedding <=> $1) as similarity
|
||||||
FROM session_memories
|
FROM memories
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY embedding <=> $1
|
ORDER BY embedding <=> $1
|
||||||
LIMIT $2`,
|
LIMIT $2`,
|
||||||
@@ -110,10 +112,10 @@ export async function memorySearch(args: MemorySearchArgs): Promise<string> {
|
|||||||
return 'No relevant memories found';
|
return 'No relevant memories found';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update times_surfaced for returned memories
|
// Update access_count for returned memories
|
||||||
const ids = memories.map(m => m.id);
|
const ids = memories.map(m => m.id);
|
||||||
await execute(
|
await execute(
|
||||||
`UPDATE session_memories SET times_surfaced = times_surfaced + 1, last_surfaced = NOW() WHERE id = ANY($1)`,
|
`UPDATE memories SET access_count = access_count + 1, last_accessed_at = NOW() WHERE id = ANY($1)`,
|
||||||
[ids]
|
[ids]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -154,9 +156,9 @@ export async function memoryList(args: MemoryListArgs): Promise<string> {
|
|||||||
params.push(limit);
|
params.push(limit);
|
||||||
|
|
||||||
const memories = await query<Memory>(
|
const memories = await query<Memory>(
|
||||||
`SELECT id, category, title, content, context, project, times_surfaced,
|
`SELECT id, category, title, content, context, project, access_count,
|
||||||
to_char(created_at, 'YYYY-MM-DD') as created_at
|
to_char(created_at, 'YYYY-MM-DD') as created_at
|
||||||
FROM session_memories
|
FROM memories
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT $${paramIndex}`,
|
LIMIT $${paramIndex}`,
|
||||||
@@ -170,8 +172,8 @@ export async function memoryList(args: MemoryListArgs): Promise<string> {
|
|||||||
const lines = [`Memories${project ? ` (${project})` : ''}:\n`];
|
const lines = [`Memories${project ? ` (${project})` : ''}:\n`];
|
||||||
for (const m of memories) {
|
for (const m of memories) {
|
||||||
const proj = m.project ? `[${m.project}] ` : '';
|
const proj = m.project ? `[${m.project}] ` : '';
|
||||||
const surfaced = m.times_surfaced > 0 ? ` (shown ${m.times_surfaced}x)` : '';
|
const accessed = m.access_count > 0 ? ` (accessed ${m.access_count}x)` : '';
|
||||||
lines.push(`• [${m.category}] ${proj}${m.title}${surfaced}`);
|
lines.push(`• [${m.category}] ${proj}${m.title}${accessed}`);
|
||||||
lines.push(` ${m.content.slice(0, 100)}${m.content.length > 100 ? '...' : ''}`);
|
lines.push(` ${m.content.slice(0, 100)}${m.content.length > 100 ? '...' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +184,7 @@ export async function memoryList(args: MemoryListArgs): Promise<string> {
|
|||||||
* Delete a memory by ID
|
* Delete a memory by ID
|
||||||
*/
|
*/
|
||||||
export async function memoryDelete(id: number): Promise<string> {
|
export async function memoryDelete(id: number): Promise<string> {
|
||||||
const result = await execute('DELETE FROM session_memories WHERE id = $1', [id]);
|
const result = await execute('DELETE FROM memories WHERE id = $1', [id]);
|
||||||
|
|
||||||
if (result === 0) {
|
if (result === 0) {
|
||||||
return `Memory not found: ${id}`;
|
return `Memory not found: ${id}`;
|
||||||
@@ -200,9 +202,9 @@ export async function memoryContext(project: string | null, taskDescription?: st
|
|||||||
// Get project-specific memories
|
// Get project-specific memories
|
||||||
if (project) {
|
if (project) {
|
||||||
const projectMemories = await query<Memory>(
|
const projectMemories = await query<Memory>(
|
||||||
`SELECT category, title, content FROM session_memories
|
`SELECT category, title, content FROM memories
|
||||||
WHERE project = $1
|
WHERE project = $1
|
||||||
ORDER BY times_surfaced DESC, created_at DESC
|
ORDER BY access_count DESC, created_at DESC
|
||||||
LIMIT 5`,
|
LIMIT 5`,
|
||||||
[project]
|
[project]
|
||||||
);
|
);
|
||||||
@@ -222,7 +224,7 @@ export async function memoryContext(project: string | null, taskDescription?: st
|
|||||||
if (embedding) {
|
if (embedding) {
|
||||||
const relevant = await query<Memory>(
|
const relevant = await query<Memory>(
|
||||||
`SELECT category, title, content, project
|
`SELECT category, title, content, project
|
||||||
FROM session_memories
|
FROM memories
|
||||||
WHERE embedding IS NOT NULL
|
WHERE embedding IS NOT NULL
|
||||||
ORDER BY embedding <=> $1
|
ORDER BY embedding <=> $1
|
||||||
LIMIT 3`,
|
LIMIT 3`,
|
||||||
@@ -241,7 +243,7 @@ export async function memoryContext(project: string | null, taskDescription?: st
|
|||||||
|
|
||||||
// Get recent gotchas (always useful)
|
// Get recent gotchas (always useful)
|
||||||
const gotchas = await query<Memory>(
|
const gotchas = await query<Memory>(
|
||||||
`SELECT title, content FROM session_memories
|
`SELECT title, content FROM memories
|
||||||
WHERE category = 'gotcha'
|
WHERE category = 'gotcha'
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 3`,
|
LIMIT 3`,
|
||||||
|
|||||||
418
src/tools/sessions.ts
Normal file
418
src/tools/sessions.ts
Normal 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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user