think-session-manager
Manages structured thinking sessions by listing, searching, retrieving detailed analysis, or resuming previous sessions for complex problem-solving.
Instructions
Manage thinking sessions: list with pagination/filtering, search by content, get detailed analysis, or resume previous sessions
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | ||
| sessionId | No | ||
| limit | No | ||
| offset | No | ||
| query | No | ||
| strategy | No | ||
| includeTopics | No | ||
| sortBy | No | timestamp | |
| completionStatus | No | ||
| minScore | No |
Implementation Reference
- index.js:1875-2085 (handler)The handler function that implements the core logic of the 'think-session-manager' tool. It processes actions ('list', 'get', 'resume', 'search') by calling methods on the shared sessionStorage instance (ThinkingSessionStorage), handling pagination, filtering, searching, and resuming sessions.async (args) => { try { switch (args.action) { case "list": const listOptions = { limit: args.limit, offset: args.offset, strategy: args.strategy, includeTopics: args.includeTopics, sortBy: args.sortBy }; const listResult = await sessionStorage.listSessions(listOptions); return { content: [{ type: "text", text: JSON.stringify({ action: "list", total: listResult.total, offset: listResult.offset, limit: listResult.limit, hasMore: listResult.hasMore, sessions: listResult.sessions.map(s => ({ sessionId: s.id.split('-').slice(-2).join('-'), // Extract timestamp portion description: s.sessionPurpose || s.topics?.[0]?.term + ' analysis' || 'General thinking session', strategy: s.strategy, quality: s.qualityRating ? Object.values(s.qualityRating).filter(v => typeof v === 'number').reduce((sum, val, _, arr) => sum + val / arr.length, 0).toFixed(1) : null, duration: s.automaticMetrics?.duration || null, thoughtCount: s.thoughtCount, completion: s.completion, complexity: s.complexity, age: s.age })) }, null, 2) }] }; case "get": if (!args.sessionId) { throw new Error("sessionId required for 'get' action"); } // Handle both full session ID and shortened version let fullSessionId = args.sessionId; if (!args.sessionId.includes('session-')) { // If it's a shortened ID, we need to find the full ID const allSessions = await sessionStorage.listSessions({ limit: 1000 }); const matchingSession = allSessions.sessions.find(s => s.id.split('-').slice(-2).join('-') === args.sessionId ); if (matchingSession) { fullSessionId = matchingSession.id; } } const session = await sessionStorage.getSession(fullSessionId); if (!session) { throw new Error(`Session not found: ${args.sessionId}`); } const sessionAnalysis = { id: session.id, strategy: session.strategy, timestamp: session.timestamp, thoughtHistory: session.thoughtHistory.map(t => ({ thoughtNumber: t.thoughtNumber, absoluteNumber: t.absoluteNumber, sequenceNumber: t.sequenceNumber, thought: t.thought.substring(0, 150) + (t.thought.length > 150 ? '...' : ''), stage: t.currentStage, hasActions: t.plannedActions ? t.plannedActions.length > 0 : false, hasResults: t.actionResults ? t.actionResults.length > 0 : false })), branches: Object.keys(session.branches), topics: sessionStorage.extractTopics(session.thoughtHistory), complexity: sessionStorage.calculateComplexity(session.thoughtHistory, session.branches), completion: sessionStorage.getCompletionStatus(session.thoughtHistory), summary: { totalThoughts: session.thoughtHistory.length, totalBranches: Object.keys(session.branches).length, finalStage: session.thoughtHistory[session.thoughtHistory.length - 1]?.currentStage, completed: !session.thoughtHistory[session.thoughtHistory.length - 1]?.nextThoughtNeeded } }; return { content: [{ type: "text", text: JSON.stringify({ action: "get", ...sessionAnalysis }, null, 2) }] }; case "search": if (!args.query) { throw new Error("query required for 'search' action"); } const searchOptions = { limit: args.limit, offset: args.offset, minScore: args.minScore, strategy: args.strategy, completionStatus: args.completionStatus }; const searchResult = await sessionStorage.searchSessions(args.query, searchOptions); return { content: [{ type: "text", text: JSON.stringify({ action: "search", query: args.query, ...searchResult, sessions: searchResult.sessions.map(s => ({ sessionId: s.id.split('-').slice(-2).join('-'), // Extract timestamp portion for shorter ID description: s.sessionPurpose || s.thoughtHistory[0]?.thought.substring(0, 100) + '...' || 'No description available', strategy: s.strategy, quality: s.qualityRating ? Object.values(s.qualityRating).filter(v => typeof v === 'number').reduce((sum, val, _, arr) => sum + val / arr.length, 0).toFixed(1) : null, duration: s.automaticMetrics?.duration || null, completion: s.completion, searchScore: s.searchScore.toFixed(2), matchedTerms: s.matchedTerms, age: `${Math.round((Date.now() - new Date(s.timestamp)) / (1000 * 60))}m ago` })) }, null, 2) }] }; case "resume": if (!args.sessionId) { throw new Error("sessionId required for 'resume' action"); } // Handle both full session ID and shortened version let fullResumeSessionId = args.sessionId; if (!args.sessionId.includes('session-')) { // If it's a shortened ID, we need to find the full ID const allSessions = await sessionStorage.listSessions({ limit: 1000 }); const matchingSession = allSessions.sessions.find(s => s.id.split('-').slice(-2).join('-') === args.sessionId ); if (matchingSession) { fullResumeSessionId = matchingSession.id; } } const resumeSession = await sessionStorage.getSession(fullResumeSessionId); if (!resumeSession) { throw new Error(`Session not found: ${args.sessionId}`); } // Restore session state thinkingServer.thoughtHistory = resumeSession.thoughtHistory; thinkingServer.branches = resumeSession.branches; thinkingServer.sessionId = resumeSession.id; thinkingServer.strategy = resumeSession.strategy; thinkingServer.stageManager = new StageManager(resumeSession.strategy); thinkingServer.isInitialized = true; // Reconstruct dual numbering from history const lastThought = resumeSession.thoughtHistory[resumeSession.thoughtHistory.length - 1]; thinkingServer.absoluteThoughtNumber = lastThought?.absoluteNumber || 0; thinkingServer.sequenceThoughtNumber = lastThought?.sequenceNumber || 0; // Set stage to last known stage if (lastThought?.currentStage) { thinkingServer.stageManager.currentStage = lastThought.currentStage; } console.error(`📚 Resumed session: ${resumeSession.id} (${resumeSession.strategy})`); // Build semantic response for current state const semanticResponse = thinkingServer.parameterRouter.buildSemanticResponse( thinkingServer.strategy, thinkingServer.stageManager.getCurrentStage(), thinkingServer.thoughtHistory, thinkingServer.sessionId ); return { content: [{ type: "text", text: JSON.stringify({ action: "resume", message: `Resumed session: ${resumeSession.id}`, strategy: resumeSession.strategy, thoughtCount: resumeSession.thoughtHistory.length, currentState: semanticResponse.currentState, absoluteThoughtNumber: thinkingServer.absoluteThoughtNumber, sequenceThoughtNumber: thinkingServer.sequenceThoughtNumber, availableActions: semanticResponse.availableActions, lastThought: lastThought?.thought?.substring(0, 200) + (lastThought?.thought?.length > 200 ? '...' : '') }, null, 2) }] }; default: throw new Error(`Unknown action: ${args.action}`); } } catch (error) { console.error(`ERROR: Session manager action '${args.action}' failed:`, error); throw error; } }
- index.js:1864-1874 (schema)Zod schema for input validation of the think-session-manager tool, defining parameters for different actions like listing, searching, and filtering sessions.action: z.enum(["list", "get", "resume", "search"]), sessionId: z.string().optional(), limit: z.number().optional().default(10), offset: z.number().optional().default(0), query: z.string().optional(), strategy: z.string().optional(), includeTopics: z.boolean().optional().default(false), sortBy: z.enum(["timestamp", "thoughtCount", "complexity", "quality", "duration"]).optional().default("timestamp"), completionStatus: z.enum(["empty", "started", "in-progress", "completed"]).optional(), minScore: z.number().optional().default(0.1) },
- index.js:1861-2087 (registration)MCP server tool registration for 'think-session-manager', including name, description, schema, and handler reference."think-session-manager", "Manage thinking sessions: list with pagination/filtering, search by content, get detailed analysis, or resume previous sessions", { action: z.enum(["list", "get", "resume", "search"]), sessionId: z.string().optional(), limit: z.number().optional().default(10), offset: z.number().optional().default(0), query: z.string().optional(), strategy: z.string().optional(), includeTopics: z.boolean().optional().default(false), sortBy: z.enum(["timestamp", "thoughtCount", "complexity", "quality", "duration"]).optional().default("timestamp"), completionStatus: z.enum(["empty", "started", "in-progress", "completed"]).optional(), minScore: z.number().optional().default(0.1) }, async (args) => { try { switch (args.action) { case "list": const listOptions = { limit: args.limit, offset: args.offset, strategy: args.strategy, includeTopics: args.includeTopics, sortBy: args.sortBy }; const listResult = await sessionStorage.listSessions(listOptions); return { content: [{ type: "text", text: JSON.stringify({ action: "list", total: listResult.total, offset: listResult.offset, limit: listResult.limit, hasMore: listResult.hasMore, sessions: listResult.sessions.map(s => ({ sessionId: s.id.split('-').slice(-2).join('-'), // Extract timestamp portion description: s.sessionPurpose || s.topics?.[0]?.term + ' analysis' || 'General thinking session', strategy: s.strategy, quality: s.qualityRating ? Object.values(s.qualityRating).filter(v => typeof v === 'number').reduce((sum, val, _, arr) => sum + val / arr.length, 0).toFixed(1) : null, duration: s.automaticMetrics?.duration || null, thoughtCount: s.thoughtCount, completion: s.completion, complexity: s.complexity, age: s.age })) }, null, 2) }] }; case "get": if (!args.sessionId) { throw new Error("sessionId required for 'get' action"); } // Handle both full session ID and shortened version let fullSessionId = args.sessionId; if (!args.sessionId.includes('session-')) { // If it's a shortened ID, we need to find the full ID const allSessions = await sessionStorage.listSessions({ limit: 1000 }); const matchingSession = allSessions.sessions.find(s => s.id.split('-').slice(-2).join('-') === args.sessionId ); if (matchingSession) { fullSessionId = matchingSession.id; } } const session = await sessionStorage.getSession(fullSessionId); if (!session) { throw new Error(`Session not found: ${args.sessionId}`); } const sessionAnalysis = { id: session.id, strategy: session.strategy, timestamp: session.timestamp, thoughtHistory: session.thoughtHistory.map(t => ({ thoughtNumber: t.thoughtNumber, absoluteNumber: t.absoluteNumber, sequenceNumber: t.sequenceNumber, thought: t.thought.substring(0, 150) + (t.thought.length > 150 ? '...' : ''), stage: t.currentStage, hasActions: t.plannedActions ? t.plannedActions.length > 0 : false, hasResults: t.actionResults ? t.actionResults.length > 0 : false })), branches: Object.keys(session.branches), topics: sessionStorage.extractTopics(session.thoughtHistory), complexity: sessionStorage.calculateComplexity(session.thoughtHistory, session.branches), completion: sessionStorage.getCompletionStatus(session.thoughtHistory), summary: { totalThoughts: session.thoughtHistory.length, totalBranches: Object.keys(session.branches).length, finalStage: session.thoughtHistory[session.thoughtHistory.length - 1]?.currentStage, completed: !session.thoughtHistory[session.thoughtHistory.length - 1]?.nextThoughtNeeded } }; return { content: [{ type: "text", text: JSON.stringify({ action: "get", ...sessionAnalysis }, null, 2) }] }; case "search": if (!args.query) { throw new Error("query required for 'search' action"); } const searchOptions = { limit: args.limit, offset: args.offset, minScore: args.minScore, strategy: args.strategy, completionStatus: args.completionStatus }; const searchResult = await sessionStorage.searchSessions(args.query, searchOptions); return { content: [{ type: "text", text: JSON.stringify({ action: "search", query: args.query, ...searchResult, sessions: searchResult.sessions.map(s => ({ sessionId: s.id.split('-').slice(-2).join('-'), // Extract timestamp portion for shorter ID description: s.sessionPurpose || s.thoughtHistory[0]?.thought.substring(0, 100) + '...' || 'No description available', strategy: s.strategy, quality: s.qualityRating ? Object.values(s.qualityRating).filter(v => typeof v === 'number').reduce((sum, val, _, arr) => sum + val / arr.length, 0).toFixed(1) : null, duration: s.automaticMetrics?.duration || null, completion: s.completion, searchScore: s.searchScore.toFixed(2), matchedTerms: s.matchedTerms, age: `${Math.round((Date.now() - new Date(s.timestamp)) / (1000 * 60))}m ago` })) }, null, 2) }] }; case "resume": if (!args.sessionId) { throw new Error("sessionId required for 'resume' action"); } // Handle both full session ID and shortened version let fullResumeSessionId = args.sessionId; if (!args.sessionId.includes('session-')) { // If it's a shortened ID, we need to find the full ID const allSessions = await sessionStorage.listSessions({ limit: 1000 }); const matchingSession = allSessions.sessions.find(s => s.id.split('-').slice(-2).join('-') === args.sessionId ); if (matchingSession) { fullResumeSessionId = matchingSession.id; } } const resumeSession = await sessionStorage.getSession(fullResumeSessionId); if (!resumeSession) { throw new Error(`Session not found: ${args.sessionId}`); } // Restore session state thinkingServer.thoughtHistory = resumeSession.thoughtHistory; thinkingServer.branches = resumeSession.branches; thinkingServer.sessionId = resumeSession.id; thinkingServer.strategy = resumeSession.strategy; thinkingServer.stageManager = new StageManager(resumeSession.strategy); thinkingServer.isInitialized = true; // Reconstruct dual numbering from history const lastThought = resumeSession.thoughtHistory[resumeSession.thoughtHistory.length - 1]; thinkingServer.absoluteThoughtNumber = lastThought?.absoluteNumber || 0; thinkingServer.sequenceThoughtNumber = lastThought?.sequenceNumber || 0; // Set stage to last known stage if (lastThought?.currentStage) { thinkingServer.stageManager.currentStage = lastThought.currentStage; } console.error(`📚 Resumed session: ${resumeSession.id} (${resumeSession.strategy})`); // Build semantic response for current state const semanticResponse = thinkingServer.parameterRouter.buildSemanticResponse( thinkingServer.strategy, thinkingServer.stageManager.getCurrentStage(), thinkingServer.thoughtHistory, thinkingServer.sessionId ); return { content: [{ type: "text", text: JSON.stringify({ action: "resume", message: `Resumed session: ${resumeSession.id}`, strategy: resumeSession.strategy, thoughtCount: resumeSession.thoughtHistory.length, currentState: semanticResponse.currentState, absoluteThoughtNumber: thinkingServer.absoluteThoughtNumber, sequenceThoughtNumber: thinkingServer.sequenceThoughtNumber, availableActions: semanticResponse.availableActions, lastThought: lastThought?.thought?.substring(0, 200) + (lastThought?.thought?.length > 200 ? '...' : '') }, null, 2) }] }; default: throw new Error(`Unknown action: ${args.action}`); } } catch (error) { console.error(`ERROR: Session manager action '${args.action}' failed:`, error); throw error; } } );
- index.js:1383-1655 (helper)ThinkingSessionStorage class providing helper methods for session persistence, listing, searching, analysis (topics, complexity, completion status), and resumption. Used by the tool handler for all session operations.class ThinkingSessionStorage { constructor(storagePath) { this.storagePath = storagePath; } calculateAutomaticMetrics(thoughtHistory, sessionStartTime, hasActions) { const sessionEndTime = new Date().toISOString(); const durationMs = new Date(sessionEndTime) - new Date(sessionStartTime); const durationMinutes = Math.round(durationMs / (1000 * 60)); // Calculate iteration ratio (revisions / total thoughts) const revisionCount = thoughtHistory.filter(t => t.isRevision).length; const iterationRatio = thoughtHistory.length > 0 ? revisionCount / thoughtHistory.length : 0; return { duration: durationMinutes, iterationRatio: Math.round(iterationRatio * 100) / 100, // Round to 2 decimals toolIntegration: hasActions }; } async saveSession(sessionId, thoughtHistory, branches, sessionPurpose = null, qualityRating = null, sessionStartTime = null) { const sessionDir = path.join(this.storagePath, sessionId); await fs.ensureDir(sessionDir); // Calculate automatic metrics const hasActions = thoughtHistory.some(t => t.plannedActions || t.actionResults); const automaticMetrics = sessionStartTime ? this.calculateAutomaticMetrics(thoughtHistory, sessionStartTime, hasActions) : null; const sessionData = { id: sessionId, timestamp: new Date().toISOString(), strategy: sessionId.split('-')[0], // Extract strategy from sessionId sessionPurpose, qualityRating, automaticMetrics, thoughtHistory, branches }; const filePath = path.join(sessionDir, 'session.json'); await fs.writeJson(filePath, sessionData, { spaces: 2 }); return filePath; } // Extract key topics and terms from thought content extractTopics(thoughtHistory) { const allText = thoughtHistory.map(t => t.thought).join(' '); // Simple topic extraction - remove common words and get meaningful terms const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'among', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'shall']); const words = allText.toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2 && !stopWords.has(word)); // Count word frequency const wordCounts = {}; words.forEach(word => { wordCounts[word] = (wordCounts[word] || 0) + 1; }); // Get top terms const topTerms = Object.entries(wordCounts) .sort(([,a], [,b]) => b - a) .slice(0, 8) .map(([word, count]) => ({ term: word, frequency: count })); return topTerms; } // Calculate session complexity based on branching, actions, and thought depth calculateComplexity(thoughtHistory, branches) { const thoughtComplexity = thoughtHistory.length; const branchComplexity = Object.keys(branches).length * 2; const actionComplexity = thoughtHistory.filter(t => t.plannedActions || t.actionResults).length; const totalComplexity = thoughtComplexity + branchComplexity + actionComplexity; if (totalComplexity < 5) return 'simple'; if (totalComplexity < 15) return 'moderate'; if (totalComplexity < 30) return 'complex'; return 'very-complex'; } // Determine completion status getCompletionStatus(thoughtHistory) { if (thoughtHistory.length === 0) return 'empty'; const lastThought = thoughtHistory[thoughtHistory.length - 1]; const hasConclusion = lastThought.finalAnswer || !lastThought.nextThoughtNeeded; const hasSignificantProgress = thoughtHistory.length >= 3; if (hasConclusion && hasSignificantProgress) return 'completed'; if (hasSignificantProgress) return 'in-progress'; return 'started'; } // Search sessions by content with term scoring async searchSessions(query, options = {}) { const { limit = 10, offset = 0, minScore = 0.1, strategy = null, completionStatus = null } = options; const sessions = await this.getAllSessions(); const queryTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2); const scoredSessions = sessions.map(session => { const sessionText = session.thoughtHistory.map(t => t.thought).join(' ').toLowerCase(); // Calculate relevance score let score = 0; queryTerms.forEach(term => { const regex = new RegExp(term, 'gi'); const matches = sessionText.match(regex) || []; score += matches.length; }); // Normalize by session length const normalizedScore = score / Math.max(sessionText.length / 1000, 1); return { ...session, searchScore: normalizedScore, matchedTerms: queryTerms.filter(term => sessionText.includes(term)) }; }); // Filter and sort by relevance let filteredSessions = scoredSessions .filter(s => s.searchScore >= minScore) .filter(s => !strategy || s.strategy === strategy) .filter(s => !completionStatus || s.completion === completionStatus) .sort((a, b) => b.searchScore - a.searchScore); const total = filteredSessions.length; filteredSessions = filteredSessions.slice(offset, offset + limit); return { sessions: filteredSessions, total, offset, limit, hasMore: offset + limit < total }; } async listSessions(options = {}) { const { limit = 10, offset = 0, strategy = null, includeTopics = false, sortBy = 'timestamp' // timestamp, thoughtCount, complexity } = options; await fs.ensureDir(this.storagePath); const dirs = await fs.readdir(this.storagePath); const sessions = []; for (const dir of dirs) { const sessionPath = path.join(this.storagePath, dir, 'session.json'); if (await fs.pathExists(sessionPath)) { try { const sessionData = await fs.readJson(sessionPath); const sessionInfo = { id: sessionData.id, timestamp: sessionData.timestamp, strategy: sessionData.strategy || "unknown", sessionPurpose: sessionData.sessionPurpose, qualityRating: sessionData.qualityRating, automaticMetrics: sessionData.automaticMetrics, thoughtCount: sessionData.thoughtHistory.length, branchCount: Object.keys(sessionData.branches).length, completion: this.getCompletionStatus(sessionData.thoughtHistory), complexity: this.calculateComplexity(sessionData.thoughtHistory, sessionData.branches), age: `${Math.round((Date.now() - new Date(sessionData.timestamp)) / (1000 * 60))} minutes ago` }; if (includeTopics) { sessionInfo.topics = this.extractTopics(sessionData.thoughtHistory); } sessions.push(sessionInfo); } catch (error) { console.error(`Error reading session ${dir}:`, error); } } } // Filter by strategy if specified const filteredSessions = strategy ? sessions.filter(s => s.strategy === strategy) : sessions; // Sort sessions filteredSessions.sort((a, b) => { switch (sortBy) { case 'thoughtCount': return b.thoughtCount - a.thoughtCount; case 'complexity': const complexityOrder = { 'simple': 1, 'moderate': 2, 'complex': 3, 'very-complex': 4 }; return complexityOrder[b.complexity] - complexityOrder[a.complexity]; case 'quality': // Calculate average quality score const getAvgQuality = (session) => { if (!session.qualityRating) return 0; const ratings = Object.values(session.qualityRating).filter(v => typeof v === 'number'); return ratings.length > 0 ? ratings.reduce((sum, val) => sum + val, 0) / ratings.length : 0; }; return getAvgQuality(b) - getAvgQuality(a); case 'duration': const getDuration = (session) => session.automaticMetrics?.duration || 0; return getDuration(a) - getDuration(b); // Shorter duration first (more efficient) case 'timestamp': default: return new Date(b.timestamp) - new Date(a.timestamp); } }); const total = filteredSessions.length; const paginatedSessions = filteredSessions.slice(offset, offset + limit); return { sessions: paginatedSessions, total, offset, limit, hasMore: offset + limit < total }; } async getAllSessions() { await fs.ensureDir(this.storagePath); const dirs = await fs.readdir(this.storagePath); const sessions = []; for (const dir of dirs) { const sessionPath = path.join(this.storagePath, dir, 'session.json'); if (await fs.pathExists(sessionPath)) { try { const sessionData = await fs.readJson(sessionPath); sessions.push({ ...sessionData, completion: this.getCompletionStatus(sessionData.thoughtHistory), complexity: this.calculateComplexity(sessionData.thoughtHistory, sessionData.branches) }); } catch (error) { console.error(`Error reading session ${dir}:`, error); } } } return sessions; } async getSession(sessionId) { const sessionPath = path.join(this.storagePath, sessionId, 'session.json'); if (await fs.pathExists(sessionPath)) { return await fs.readJson(sessionPath); } return null; } }