Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/sequentialthinking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,19 @@ Add this to your `claude_desktop_config.json`:
}
```

To disable logging of thought information set env var: `DISABLE_THOUGHT_LOGGING` to `true`.
Comment
## Environment Variables

- `DISABLE_THOUGHT_LOGGING`: Set to `true` to disable stderr logging of thought information (default: `false`)
- `MAX_THOUGHT_HISTORY`: Maximum number of thoughts to retain in memory (default: `1000`)
- `MAX_BRANCHES`: Maximum number of thought branches to track simultaneously (default: `100`)

**Note on Memory Management**: The server implements bounded state to prevent unbounded memory growth in long-running sessions. When limits are exceeded, the oldest thoughts/branches are evicted using FIFO (First-In-First-Out) strategy.

## Architecture Notes

This server uses a singleton pattern appropriate for stdio transport, where each client spawns a dedicated server process. The singleton maintains state across multiple thought requests within a single thinking session, enabling features like thought history tracking and branch management.

For stdio transport, concurrent requests from the same client are rare (LLMs typically call tools sequentially), so the singleton design provides the necessary state persistence without significant concurrency concerns.

### Usage with VS Code

Expand Down
265 changes: 265 additions & 0 deletions src/sequentialthinking/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,190 @@ describe('SequentialThinkingServer', () => {

expect(data.nextThoughtNeeded).toBe(false);
});

it('should reject negative thoughtNumber', () => {
const input = {
thought: 'Test thought',
thoughtNumber: -1,
totalThoughts: 3,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('thoughtNumber must be >= 1');
});

it('should reject zero thoughtNumber', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 0,
totalThoughts: 3,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('thoughtNumber must be >= 1');
});

it('should reject negative totalThoughts', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: -5,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('totalThoughts must be >= 1');
});

it('should reject zero totalThoughts', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: 0,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('totalThoughts must be >= 1');
});

it('should reject NaN thoughtNumber', () => {
const input = {
thought: 'Test thought',
thoughtNumber: NaN,
totalThoughts: 3,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('thoughtNumber must be a valid number');
});

it('should reject Infinity totalThoughts', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: Infinity,
nextThoughtNeeded: true
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('totalThoughts must be a valid number');
});

it('should reject non-boolean isRevision', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: 'true'
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('isRevision must be a boolean');
});

it('should reject non-number revisesThought', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: true,
revisesThought: '1'
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('revisesThought must be a number');
});

it('should reject negative revisesThought', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: true,
revisesThought: -1
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('revisesThought must be >= 1');
});

it('should reject non-number branchFromThought', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
branchFromThought: '1',
branchId: 'branch-a'
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('branchFromThought must be a number');
});

it('should reject non-string branchId', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
branchFromThought: 1,
branchId: 123
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('branchId must be a string');
});

it('should reject empty branchId', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
branchFromThought: 1,
branchId: ''
};

const result = server.processThought(input);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('branchId cannot be empty');
});

it('should accept valid optional fields', () => {
const input = {
thought: 'Test thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true,
isRevision: true,
revisesThought: 1,
branchFromThought: 1,
branchId: 'branch-a',
needsMoreThoughts: true
};

const result = server.processThought(input);
expect(result.isError).toBeUndefined();
});
});

describe('processThought - response format', () => {
Expand Down Expand Up @@ -436,4 +620,85 @@ describe('SequentialThinkingServer', () => {
expect(result.isError).toBeUndefined();
});
});

describe('processThought - memory bounds enforcement', () => {
it('should enforce maximum history size with default 1000 limit', () => {
// Use default MAX_THOUGHT_HISTORY (1000)
// Add 1005 thoughts to exceed the default limit
for (let i = 1; i <= 1005; i++) {
server.processThought({
thought: `Thought ${i}`,
thoughtNumber: i,
totalThoughts: 1005,
nextThoughtNeeded: i < 1005
});
}

const result = server.processThought({
thought: 'Final check thought',
thoughtNumber: 1006,
totalThoughts: 1006,
nextThoughtNeeded: false
});

const data = JSON.parse(result.content[0].text);

// History should be capped at 1000 (the default MAX_THOUGHT_HISTORY)
expect(data.thoughtHistoryLength).toBeLessThanOrEqual(1000);
});

it('should enforce maximum branches limit with default 100 limit', () => {
// Use default MAX_BRANCHES (100)
// Create 105 branches to exceed the default limit
for (let i = 1; i <= 105; i++) {
server.processThought({
thought: `Branch ${i} thought`,
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false,
branchFromThought: 1,
branchId: `branch-${i}`
});
}

const result = server.processThought({
thought: 'Final thought',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
});

const data = JSON.parse(result.content[0].text);

// Branches should be capped at 100 (the default MAX_BRANCHES)
expect(data.branches.length).toBeLessThanOrEqual(100);
});

it('should use FIFO eviction for thought history', () => {
const testServer = new SequentialThinkingServer();

// Add thoughts up to limit + 2 to trigger eviction
// Using small number for faster test
for (let i = 1; i <= 5; i++) {
testServer.processThought({
thought: `Thought ${i}`,
thoughtNumber: i,
totalThoughts: 5,
nextThoughtNeeded: i < 5
});
}

const result = testServer.processThought({
thought: 'Thought 6',
thoughtNumber: 6,
totalThoughts: 6,
nextThoughtNeeded: false
});

const data = JSON.parse(result.content[0].text);

// With default limit of 1000, all 6 thoughts should be retained
expect(data.thoughtHistoryLength).toBe(6);
});
});
});
Loading