From ceb238175d61f03f906614a88a6b88fef1441f56 Mon Sep 17 00:00:00 2001 From: Limitless2023 Date: Wed, 11 Feb 2026 16:35:46 +0800 Subject: [PATCH] fix(sequentialthinking): prevent unbounded memory growth in thought history The SequentialThinkingServer stores all thoughts in memory arrays that grow without bound across the lifetime of the server process. In long-running sessions (6-8+ hours), this can consume 10GB+ of RAM. Changes: - Add configurable max history limit (SEQUENTIAL_THINKING_MAX_HISTORY env var, default 1000) with automatic trimming of oldest thoughts - Add clearHistory() method to explicitly free memory - Register 'sequentialthinking_clear' tool so clients can reset state - Add tests for memory management and history clearing Fixes #2912 --- src/sequentialthinking/README.md | 7 +- src/sequentialthinking/__tests__/lib.test.ts | 75 ++++++++++++++++++++ src/sequentialthinking/index.ts | 15 ++++ src/sequentialthinking/lib.ts | 35 +++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/sequentialthinking/README.md b/src/sequentialthinking/README.md index 322ded2726..7488926514 100644 --- a/src/sequentialthinking/README.md +++ b/src/sequentialthinking/README.md @@ -77,7 +77,12 @@ Add this to your `claude_desktop_config.json`: } ``` -To disable logging of thought information set env var: `DISABLE_THOUGHT_LOGGING` to `true`. +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `DISABLE_THOUGHT_LOGGING` | Set to `true` to disable logging thought output to stderr | `false` | +| `SEQUENTIAL_THINKING_MAX_HISTORY` | Maximum number of thoughts retained in memory. Oldest thoughts are trimmed when exceeded. | `1000` | Comment ### Usage with VS Code diff --git a/src/sequentialthinking/__tests__/lib.test.ts b/src/sequentialthinking/__tests__/lib.test.ts index 2114c5ec18..7712618b68 100644 --- a/src/sequentialthinking/__tests__/lib.test.ts +++ b/src/sequentialthinking/__tests__/lib.test.ts @@ -251,6 +251,81 @@ describe('SequentialThinkingServer', () => { }); }); + describe('clearHistory', () => { + it('should clear all thoughts and branches', () => { + server.processThought({ + thought: 'First', thoughtNumber: 1, totalThoughts: 3, nextThoughtNeeded: true + }); + server.processThought({ + thought: 'Branch', thoughtNumber: 2, totalThoughts: 3, nextThoughtNeeded: true, + branchFromThought: 1, branchId: 'b1' + }); + + const result = server.clearHistory(); + const data = JSON.parse(result.content[0].text); + + expect(data.cleared).toBe(true); + expect(data.previousThoughtCount).toBe(2); + expect(data.previousBranchCount).toBe(1); + + // Verify history is actually empty + const nextResult = server.processThought({ + thought: 'After clear', thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false + }); + const nextData = JSON.parse(nextResult.content[0].text); + expect(nextData.thoughtHistoryLength).toBe(1); + expect(nextData.branches).toEqual([]); + }); + }); + + describe('memory management', () => { + it('should trim history when exceeding max limit', () => { + // Default max is 1000, but we can test the trimming behavior + // by adding more thoughts than the limit + const maxHistory = 1000; + for (let i = 1; i <= maxHistory + 50; i++) { + server.processThought({ + thought: `Thought ${i}`, + thoughtNumber: i, + totalThoughts: maxHistory + 50, + nextThoughtNeeded: i < maxHistory + 50 + }); + } + + const lastResult = server.processThought({ + thought: 'Final check', + thoughtNumber: maxHistory + 51, + totalThoughts: maxHistory + 51, + nextThoughtNeeded: false + }); + const data = JSON.parse(lastResult.content[0].text); + // History should be capped at maxHistory + expect(data.thoughtHistoryLength).toBeLessThanOrEqual(maxHistory); + }); + + it('should respect SEQUENTIAL_THINKING_MAX_HISTORY env var', () => { + process.env.SEQUENTIAL_THINKING_MAX_HISTORY = '5'; + const limitedServer = new SequentialThinkingServer(); + + for (let i = 1; i <= 10; i++) { + limitedServer.processThought({ + thought: `Thought ${i}`, + thoughtNumber: i, + totalThoughts: 10, + nextThoughtNeeded: i < 10 + }); + } + + const result = limitedServer.processThought({ + thought: 'Check', thoughtNumber: 11, totalThoughts: 11, nextThoughtNeeded: false + }); + const data = JSON.parse(result.content[0].text); + expect(data.thoughtHistoryLength).toBeLessThanOrEqual(5); + + delete process.env.SEQUENTIAL_THINKING_MAX_HISTORY; + }); + }); + describe('processThought - with logging enabled', () => { let serverWithLogging: SequentialThinkingServer; diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 809086a94c..429e5503a4 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -106,6 +106,21 @@ You should: } ); +server.registerTool( + "sequentialthinking_clear", + { + title: "Clear Thinking History", + description: + "Clears all stored thought history and branch data to free memory. " + + "Use this after completing a thinking session to prevent memory buildup " + + "in long-running server instances.", + inputSchema: {}, + }, + async () => { + return thinkingServer.clearHistory(); + } +); + async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/sequentialthinking/lib.ts b/src/sequentialthinking/lib.ts index 31a1098644..94c6be1b21 100644 --- a/src/sequentialthinking/lib.ts +++ b/src/sequentialthinking/lib.ts @@ -12,13 +12,20 @@ export interface ThoughtData { nextThoughtNeeded: boolean; } +// Default maximum number of thoughts to retain in history. +// Can be overridden via the SEQUENTIAL_THINKING_MAX_HISTORY environment variable. +const DEFAULT_MAX_HISTORY = 1000; + export class SequentialThinkingServer { private thoughtHistory: ThoughtData[] = []; private branches: Record = {}; private disableThoughtLogging: boolean; + private maxHistory: number; constructor() { this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true"; + const envMax = parseInt(process.env.SEQUENTIAL_THINKING_MAX_HISTORY || "", 10); + this.maxHistory = Number.isFinite(envMax) && envMax > 0 ? envMax : DEFAULT_MAX_HISTORY; } private formatThought(thoughtData: ThoughtData): string { @@ -49,6 +56,27 @@ export class SequentialThinkingServer { └${border}┘`; } + /** + * Clears all stored thought history and branch data. + * Useful for freeing memory in long-running sessions. + */ + public clearHistory(): { content: Array<{ type: "text"; text: string }> } { + const previousLength = this.thoughtHistory.length; + const previousBranches = Object.keys(this.branches).length; + this.thoughtHistory = []; + this.branches = {}; + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + cleared: true, + previousThoughtCount: previousLength, + previousBranchCount: previousBranches + }, null, 2) + }] + }; + } + public processThought(input: ThoughtData): { content: Array<{ type: "text"; text: string }>; isError?: boolean } { try { // Validation happens at the tool registration layer via Zod @@ -59,6 +87,13 @@ export class SequentialThinkingServer { this.thoughtHistory.push(input); + // Trim history when it exceeds the configured maximum to prevent + // unbounded memory growth during long-running sessions (see #2912). + if (this.thoughtHistory.length > this.maxHistory) { + const excess = this.thoughtHistory.length - this.maxHistory; + this.thoughtHistory.splice(0, excess); + } + if (input.branchFromThought && input.branchId) { if (!this.branches[input.branchId]) { this.branches[input.branchId] = [];