/**
* Sequential Thinking Library
* Core logic for managing thought sequences
*/
import { StoredThought, ThoughtInput, ThoughtSequence, ThoughtResponse } from './types.js';
import { generateSecureSessionId } from './utils/crypto.js';
import { validateThoughtInput, ValidationError } from './utils/validators.js';
export class SequentialThinkingManager {
private sessions: Map<string, ThoughtSequence>;
private currentSessionId: string;
private cleanupInterval: NodeJS.Timeout | null;
// Configuration
private readonly SESSION_TTL = 3600000; // 1 hour in milliseconds
private readonly CLEANUP_INTERVAL = 600000; // 10 minutes in milliseconds
private readonly MAX_SESSIONS = 10000; // Prevent unbounded growth
constructor() {
this.sessions = new Map();
this.currentSessionId = generateSecureSessionId();
this.initializeSession(this.currentSessionId);
this.cleanupInterval = null;
// Start cleanup interval
this.startCleanup();
}
/**
* Start periodic cleanup of expired sessions
*/
private startCleanup(): void {
if (this.cleanupInterval) {
return;
}
this.cleanupInterval = setInterval(() => {
this.cleanupExpiredSessions();
}, this.CLEANUP_INTERVAL);
// Don't prevent process from exiting
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
}
/**
* Stop cleanup interval
*/
private stopCleanup(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
/**
* Clean up expired sessions
*/
private cleanupExpiredSessions(): void {
const now = Date.now();
let cleaned = 0;
for (const [id, session] of this.sessions.entries()) {
if (now - session.updatedAt > this.SESSION_TTL) {
this.sessions.delete(id);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`Cleaned up ${cleaned} expired sessions. Active sessions: ${this.sessions.size}`);
}
// Emergency cleanup if we're still over the limit
if (this.sessions.size > this.MAX_SESSIONS) {
this.emergencyCleanup();
}
}
/**
* Emergency cleanup: remove oldest sessions if limit exceeded
*/
private emergencyCleanup(): void {
const sessions = Array.from(this.sessions.entries())
.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
const toRemove = sessions.slice(0, Math.floor(this.MAX_SESSIONS * 0.2)); // Remove oldest 20%
for (const [id] of toRemove) {
this.sessions.delete(id);
}
console.warn(
`Emergency cleanup: removed ${toRemove.length} oldest sessions. Active: ${this.sessions.size}`
);
}
private initializeSession(sessionId: string): void {
this.sessions.set(sessionId, {
sessionId,
thoughts: [],
branches: new Map(),
createdAt: Date.now(),
updatedAt: Date.now(),
});
}
public addThought(input: ThoughtInput, sessionId?: string): ThoughtResponse {
// Validate input
validateThoughtInput(input);
const sid = sessionId || this.currentSessionId;
let session = this.sessions.get(sid);
if (!session) {
this.initializeSession(sid);
session = this.sessions.get(sid)!;
}
const storedThought: StoredThought = {
...input,
timestamp: Date.now(),
sessionId: sid,
};
// Handle branching
if (input.branchId && input.branchFromThought !== undefined) {
let branchThoughts = session.branches.get(input.branchId);
if (!branchThoughts) {
branchThoughts = [];
session.branches.set(input.branchId, branchThoughts);
}
branchThoughts.push(storedThought);
} else {
// Regular thought in main sequence
session.thoughts.push(storedThought);
}
session.updatedAt = Date.now();
// Build response message
let message = `Thought ${input.thoughtNumber}`;
if (input.isRevision && input.revisesThought !== undefined) {
message += ` (revising thought ${input.revisesThought})`;
}
if (input.branchId) {
message += ` [branch: ${input.branchId}]`;
}
message += `: ${input.thought}`;
return {
success: true,
thoughtNumber: input.thoughtNumber,
message,
sequence: session.thoughts,
totalThoughts: input.totalThoughts,
};
}
public getSequence(sessionId?: string): StoredThought[] {
const sid = sessionId || this.currentSessionId;
const session = this.sessions.get(sid);
return session ? session.thoughts : [];
}
public getBranch(branchId: string, sessionId?: string): StoredThought[] {
const sid = sessionId || this.currentSessionId;
const session = this.sessions.get(sid);
return session?.branches.get(branchId) || [];
}
public getAllBranches(sessionId?: string): Map<string, StoredThought[]> {
const sid = sessionId || this.currentSessionId;
const session = this.sessions.get(sid);
return session?.branches || new Map();
}
public resetSession(): string {
this.currentSessionId = generateSecureSessionId();
this.initializeSession(this.currentSessionId);
return this.currentSessionId;
}
public getCurrentSessionId(): string {
return this.currentSessionId;
}
public getSessionSummary(sessionId?: string): {
sessionId: string;
thoughtCount: number;
branchCount: number;
createdAt: number;
updatedAt: number;
} | null {
const sid = sessionId || this.currentSessionId;
const session = this.sessions.get(sid);
if (!session) {
return null;
}
return {
sessionId: session.sessionId,
thoughtCount: session.thoughts.length,
branchCount: session.branches.size,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
};
}
/**
* Get statistics about all sessions
*/
public getStats(): {
totalSessions: number;
totalThoughts: number;
oldestSession: number | null;
newestSession: number | null;
} {
let totalThoughts = 0;
let oldestSession: number | null = null;
let newestSession: number | null = null;
for (const session of this.sessions.values()) {
totalThoughts += session.thoughts.length;
if (!oldestSession || session.createdAt < oldestSession) {
oldestSession = session.createdAt;
}
if (!newestSession || session.createdAt > newestSession) {
newestSession = session.createdAt;
}
}
return {
totalSessions: this.sessions.size,
totalThoughts,
oldestSession,
newestSession,
};
}
/**
* Clean up and destroy this manager
*/
public destroy(): void {
this.stopCleanup();
this.sessions.clear();
}
}