mcp-memory-keeper 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -17
- package/README.md +26 -0
- package/dist/__tests__/integration/issue-token-limit-channel-query.test.js +128 -0
- package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/index.js +36 -84
- package/dist/utils/token-limits.js +350 -0
- package/package.json +3 -3
- package/dist/__tests__/integration/cross-session-sharing.test.js +0 -302
- package/dist/index.phase1.backup.js +0 -410
- package/dist/index.phase2.backup.js +0 -704
- package/dist/server.js +0 -384
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.11.0] - 2025-12-10
|
|
11
|
+
|
|
12
|
+
### Breaking Changes
|
|
13
|
+
|
|
14
|
+
- **Node.js 18 support dropped** - Minimum required Node.js version is now 20.0.0
|
|
15
|
+
- Node.js 18 reached End-of-Life on April 30, 2025
|
|
16
|
+
- Users on Node.js 18 must upgrade to Node.js 20 or later
|
|
17
|
+
- Existing installations will continue working until updated
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Installation fails on Node.js 24 (#28)** - Updated `better-sqlite3` dependency
|
|
22
|
+
- Upgraded from `^11.10.0` to `^12.1.0` to support Node.js 24 (LTS "Krypton")
|
|
23
|
+
- Prebuilt binaries now available for Node.js 20, 22, and 24
|
|
24
|
+
- Resolves `gyp ERR!` build failures on Node.js 24
|
|
25
|
+
|
|
26
|
+
## [0.10.2] - 2025-09-16
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Critical Token Limit Issue (#24)** - Fixed token overflow with includeMetadata
|
|
31
|
+
- Implemented dynamic token limit calculation based on actual content size
|
|
32
|
+
- Automatically adjusts item limits based on average item size in session
|
|
33
|
+
- More accurate token estimation (3.5 chars/token vs 4)
|
|
34
|
+
- Configurable via environment variables (MCP_MAX_TOKENS, MCP_TOKEN_SAFETY_BUFFER)
|
|
35
|
+
- Added tokenInfo to response metadata for transparency
|
|
36
|
+
- Resolves "MCP tool context_get response exceeds maximum allowed tokens" errors
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Token Limit Management Module** (`utils/token-limits.ts`)
|
|
41
|
+
- Dynamic calculation of safe item limits
|
|
42
|
+
- Response overhead estimation
|
|
43
|
+
- Configurable token limits via environment
|
|
44
|
+
- Better visibility into token usage
|
|
45
|
+
- Proper TypeScript interfaces for context items
|
|
46
|
+
- Environment variable validation with bounds checking
|
|
47
|
+
- Safe JSON parsing with error handling
|
|
48
|
+
- Well-documented constants replacing magic numbers
|
|
49
|
+
|
|
50
|
+
## [0.10.1] - 2025-07-11
|
|
11
51
|
|
|
12
52
|
### Fixed
|
|
13
53
|
|
|
@@ -63,7 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
63
103
|
- Added recipes for common patterns in RECIPES.md
|
|
64
104
|
- Added troubleshooting tips for new features
|
|
65
105
|
|
|
66
|
-
## [0.10.0] - 2025-
|
|
106
|
+
## [0.10.0] - 2025-06-26
|
|
67
107
|
|
|
68
108
|
### Added
|
|
69
109
|
|
|
@@ -102,7 +142,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
142
|
- Enhanced validation for channel names
|
|
103
143
|
- Backward compatible - existing items default to 'default' channel
|
|
104
144
|
|
|
105
|
-
## [0.9.0] - 2025-
|
|
145
|
+
## [0.9.0] - 2025-06-21
|
|
106
146
|
|
|
107
147
|
### Changed (BREAKING)
|
|
108
148
|
|
|
@@ -125,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
125
165
|
- `context_get_shared` tool (use `context_get` instead)
|
|
126
166
|
- Complex sharing mechanism that was causing inconsistencies
|
|
127
167
|
|
|
128
|
-
## [0.8.4] - 2025-
|
|
168
|
+
## [0.8.4] - 2025-06-19
|
|
129
169
|
|
|
130
170
|
### Fixed
|
|
131
171
|
|
|
@@ -139,7 +179,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
139
179
|
- Feature flags system (planned)
|
|
140
180
|
- Database migration system (planned)
|
|
141
181
|
|
|
142
|
-
## [0.8.3] - 2025-
|
|
182
|
+
## [0.8.3] - 2025-06-19
|
|
143
183
|
|
|
144
184
|
### Added
|
|
145
185
|
|
|
@@ -160,7 +200,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
160
200
|
|
|
161
201
|
- Automatic schema migration for existing databases to add the `working_directory` column
|
|
162
202
|
|
|
163
|
-
## [0.8.0] -
|
|
203
|
+
## [0.8.0] - 2025-06-18
|
|
164
204
|
|
|
165
205
|
### Added
|
|
166
206
|
|
|
@@ -200,7 +240,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
200
240
|
- New tables: `journal_entries`, `compressed_context`, `tool_events`
|
|
201
241
|
- All 255 tests passing
|
|
202
242
|
|
|
203
|
-
## [0.7.0] -
|
|
243
|
+
## [0.7.0] - 2025-06-18
|
|
204
244
|
|
|
205
245
|
### Added
|
|
206
246
|
|
|
@@ -224,7 +264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
224
264
|
- Added comprehensive test coverage (30 new tests)
|
|
225
265
|
- All 236 tests passing
|
|
226
266
|
|
|
227
|
-
## [0.6.0] -
|
|
267
|
+
## [0.6.0] - 2025-06-17
|
|
228
268
|
|
|
229
269
|
### Added
|
|
230
270
|
|
|
@@ -247,7 +287,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
247
287
|
- Comprehensive test coverage for semantic search
|
|
248
288
|
- All 206 tests passing
|
|
249
289
|
|
|
250
|
-
## [0.5.0] -
|
|
290
|
+
## [0.5.0] - 2025-06-17
|
|
251
291
|
|
|
252
292
|
### Added
|
|
253
293
|
|
|
@@ -270,7 +310,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
270
310
|
- Added `knowledge-graph.ts` utility module
|
|
271
311
|
- Comprehensive test coverage for graph operations
|
|
272
312
|
|
|
273
|
-
## [0.4.2] -
|
|
313
|
+
## [0.4.2] - 2025-06-17
|
|
274
314
|
|
|
275
315
|
### Added
|
|
276
316
|
|
|
@@ -284,7 +324,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
284
324
|
- Git integration error handling
|
|
285
325
|
- Session list date filtering
|
|
286
326
|
|
|
287
|
-
## [0.4.1] -
|
|
327
|
+
## [0.4.1] - 2025-06-17
|
|
288
328
|
|
|
289
329
|
### Fixed
|
|
290
330
|
|
|
@@ -297,7 +337,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
297
337
|
- Improved error messages for better debugging
|
|
298
338
|
- Enhanced validation for file paths
|
|
299
339
|
|
|
300
|
-
## [0.4.0] -
|
|
340
|
+
## [0.4.0] - 2025-06-17
|
|
301
341
|
|
|
302
342
|
### Added
|
|
303
343
|
|
|
@@ -318,7 +358,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
318
358
|
- Created `git.ts` utility module
|
|
319
359
|
- 97% test coverage maintained
|
|
320
360
|
|
|
321
|
-
## [0.3.0] -
|
|
361
|
+
## [0.3.0] - 2025-06-17
|
|
322
362
|
|
|
323
363
|
### Added
|
|
324
364
|
|
|
@@ -347,7 +387,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
347
387
|
- Implemented streaming for large exports
|
|
348
388
|
- Transaction support for atomic operations
|
|
349
389
|
|
|
350
|
-
## [0.2.0] -
|
|
390
|
+
## [0.2.0] - 2025-06-17
|
|
351
391
|
|
|
352
392
|
### Added
|
|
353
393
|
|
|
@@ -377,7 +417,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
377
417
|
- Memory leak in file cache operations
|
|
378
418
|
- Session switching race condition
|
|
379
419
|
|
|
380
|
-
## [0.1.0] -
|
|
420
|
+
## [0.1.0] - 2025-06-17
|
|
381
421
|
|
|
382
422
|
### Added
|
|
383
423
|
|
|
@@ -405,12 +445,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
405
445
|
|
|
406
446
|
## Development Releases
|
|
407
447
|
|
|
408
|
-
### [0.1.0-beta.2] -
|
|
448
|
+
### [0.1.0-beta.2] - 2025-06-17
|
|
409
449
|
|
|
410
450
|
- Fixed Windows path handling
|
|
411
451
|
- Added Node.js 18+ compatibility
|
|
412
452
|
|
|
413
|
-
### [0.1.0-beta.1] -
|
|
453
|
+
### [0.1.0-beta.1] - 2025-06-17
|
|
414
454
|
|
|
415
455
|
- Initial beta release
|
|
416
456
|
- Basic functionality testing
|
package/README.md
CHANGED
|
@@ -183,10 +183,36 @@ claude mcp add memory-keeper node /absolute/path/to/mcp-memory-keeper/dist/index
|
|
|
183
183
|
|
|
184
184
|
### Environment Variables
|
|
185
185
|
|
|
186
|
+
#### Storage and Installation
|
|
187
|
+
|
|
186
188
|
- `DATA_DIR` - Directory for database storage (default: `~/mcp-data/memory-keeper/`)
|
|
187
189
|
- `MEMORY_KEEPER_INSTALL_DIR` - Installation directory (default: `~/.local/mcp-servers/memory-keeper/`)
|
|
188
190
|
- `MEMORY_KEEPER_AUTO_UPDATE` - Set to `1` to enable auto-updates
|
|
189
191
|
|
|
192
|
+
#### Token Limit Configuration
|
|
193
|
+
|
|
194
|
+
- `MCP_MAX_TOKENS` - Maximum tokens allowed in responses (default: `25000`, range: `1000-100000`)
|
|
195
|
+
- Adjust this if your MCP client has different limits
|
|
196
|
+
- `MCP_TOKEN_SAFETY_BUFFER` - Safety buffer percentage (default: `0.8`, range: `0.1-1.0`)
|
|
197
|
+
- Uses only this fraction of the max tokens to prevent overflows
|
|
198
|
+
- `MCP_MIN_ITEMS` - Minimum items to return even if exceeding limits (default: `1`, range: `1-100`)
|
|
199
|
+
- Ensures at least some results are returned
|
|
200
|
+
- `MCP_MAX_ITEMS` - Maximum items allowed per response (default: `100`, range: `10-1000`)
|
|
201
|
+
- Upper bound for result sets regardless of token limits
|
|
202
|
+
- `MCP_CHARS_PER_TOKEN` - Characters per token ratio (default: `3.5`, range: `2.5-5.0`) **[Advanced]**
|
|
203
|
+
- Adjusts token estimation accuracy for different content types
|
|
204
|
+
- Lower values = more conservative (safer but returns fewer items)
|
|
205
|
+
- Higher values = more aggressive (returns more items but risks overflow)
|
|
206
|
+
|
|
207
|
+
Example configuration for stricter token limits:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
export MCP_MAX_TOKENS=20000 # Lower max tokens
|
|
211
|
+
export MCP_TOKEN_SAFETY_BUFFER=0.7 # More conservative buffer
|
|
212
|
+
export MCP_MAX_ITEMS=50 # Fewer items per response
|
|
213
|
+
export MCP_CHARS_PER_TOKEN=3.0 # More conservative estimation (optional)
|
|
214
|
+
```
|
|
215
|
+
|
|
190
216
|
### Claude Code (CLI)
|
|
191
217
|
|
|
192
218
|
#### Configuration Scopes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test for token limit issue with channel queries
|
|
4
|
+
*
|
|
5
|
+
* Reproduces the bug where context_get with limit: 50 still exceeds token limits
|
|
6
|
+
* when querying a channel with large items.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const globals_1 = require("@jest/globals");
|
|
10
|
+
const database_js_1 = require("../../utils/database.js");
|
|
11
|
+
const RepositoryManager_js_1 = require("../../repositories/RepositoryManager.js");
|
|
12
|
+
const token_limits_js_1 = require("../../utils/token-limits.js");
|
|
13
|
+
(0, globals_1.describe)('Token Limit with Channel Query Bug', () => {
|
|
14
|
+
let dbManager;
|
|
15
|
+
let repositories;
|
|
16
|
+
let sessionId;
|
|
17
|
+
(0, globals_1.beforeEach)(() => {
|
|
18
|
+
dbManager = new database_js_1.DatabaseManager({ filename: ':memory:' });
|
|
19
|
+
repositories = new RepositoryManager_js_1.RepositoryManager(dbManager);
|
|
20
|
+
const session = repositories.sessions.create({
|
|
21
|
+
name: 'Test Session',
|
|
22
|
+
defaultChannel: 'test-channel',
|
|
23
|
+
});
|
|
24
|
+
sessionId = session.id;
|
|
25
|
+
});
|
|
26
|
+
(0, globals_1.afterEach)(() => {
|
|
27
|
+
dbManager.close();
|
|
28
|
+
});
|
|
29
|
+
(0, globals_1.it)('should enforce token limits even when limit parameter is provided', () => {
|
|
30
|
+
// Create 50 large context items (each ~1000 chars)
|
|
31
|
+
const largeValue = 'x'.repeat(1000);
|
|
32
|
+
for (let i = 0; i < 50; i++) {
|
|
33
|
+
repositories.contexts.save(sessionId, {
|
|
34
|
+
key: `large-item-${i}`,
|
|
35
|
+
value: largeValue,
|
|
36
|
+
channel: 'outbound-call-center',
|
|
37
|
+
priority: 'normal',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Query with limit: 50
|
|
41
|
+
const result = repositories.contexts.queryEnhanced({
|
|
42
|
+
sessionId,
|
|
43
|
+
channel: 'outbound-call-center',
|
|
44
|
+
limit: 50,
|
|
45
|
+
sort: 'updated_desc',
|
|
46
|
+
includeMetadata: false,
|
|
47
|
+
});
|
|
48
|
+
(0, globals_1.expect)(result.items.length).toBe(50);
|
|
49
|
+
// Check if response would exceed token limit
|
|
50
|
+
const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
|
|
51
|
+
const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
|
|
52
|
+
// Build the actual response structure
|
|
53
|
+
const response = {
|
|
54
|
+
items: result.items,
|
|
55
|
+
pagination: {
|
|
56
|
+
total: result.totalCount,
|
|
57
|
+
returned: result.items.length,
|
|
58
|
+
offset: 0,
|
|
59
|
+
hasMore: false,
|
|
60
|
+
nextOffset: null,
|
|
61
|
+
truncated: false,
|
|
62
|
+
truncatedCount: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const responseJson = JSON.stringify(response, null, 2);
|
|
66
|
+
const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
|
|
67
|
+
// The bug: even with limit: 50, the response can exceed token limits
|
|
68
|
+
if (actualTokens > tokenConfig.mcpMaxTokens) {
|
|
69
|
+
// BUG REPRODUCED: The response exceeds token limits
|
|
70
|
+
// Verify that checkTokenLimit correctly detected the issue
|
|
71
|
+
(0, globals_1.expect)(exceedsLimit).toBe(true);
|
|
72
|
+
(0, globals_1.expect)(safeItemCount).toBeLessThan(50);
|
|
73
|
+
(0, globals_1.expect)(actualTokens).toBeGreaterThan(tokenConfig.mcpMaxTokens);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
(0, globals_1.it)('should respect token limits over user-provided limit parameter', () => {
|
|
77
|
+
// Create 100 large context items (each ~800 chars)
|
|
78
|
+
const largeValue = 'y'.repeat(800);
|
|
79
|
+
for (let i = 0; i < 100; i++) {
|
|
80
|
+
repositories.contexts.save(sessionId, {
|
|
81
|
+
key: `item-${i}`,
|
|
82
|
+
value: largeValue,
|
|
83
|
+
channel: 'test-channel',
|
|
84
|
+
priority: 'normal',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Query with limit: 50 (user expectation)
|
|
88
|
+
const result = repositories.contexts.queryEnhanced({
|
|
89
|
+
sessionId,
|
|
90
|
+
channel: 'test-channel',
|
|
91
|
+
limit: 50,
|
|
92
|
+
sort: 'created_desc',
|
|
93
|
+
includeMetadata: false,
|
|
94
|
+
});
|
|
95
|
+
// Simulate the context_get handler logic
|
|
96
|
+
const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
|
|
97
|
+
const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
|
|
98
|
+
let actualItems = result.items;
|
|
99
|
+
let wasTruncated = false;
|
|
100
|
+
if (exceedsLimit && safeItemCount < result.items.length) {
|
|
101
|
+
actualItems = result.items.slice(0, safeItemCount);
|
|
102
|
+
wasTruncated = true;
|
|
103
|
+
}
|
|
104
|
+
// Build response
|
|
105
|
+
const response = {
|
|
106
|
+
items: actualItems,
|
|
107
|
+
pagination: {
|
|
108
|
+
total: result.totalCount,
|
|
109
|
+
returned: actualItems.length,
|
|
110
|
+
offset: 0,
|
|
111
|
+
hasMore: wasTruncated || actualItems.length < result.totalCount,
|
|
112
|
+
nextOffset: wasTruncated ? actualItems.length : null,
|
|
113
|
+
truncated: wasTruncated,
|
|
114
|
+
truncatedCount: wasTruncated ? result.items.length - actualItems.length : 0,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const responseJson = JSON.stringify(response, null, 2);
|
|
118
|
+
const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
|
|
119
|
+
// Verify token limit is not exceeded
|
|
120
|
+
(0, globals_1.expect)(actualTokens).toBeLessThanOrEqual(tokenConfig.mcpMaxTokens);
|
|
121
|
+
// Verify truncation occurred if needed
|
|
122
|
+
if (exceedsLimit) {
|
|
123
|
+
(0, globals_1.expect)(wasTruncated).toBe(true);
|
|
124
|
+
(0, globals_1.expect)(actualItems.length).toBeLessThan(50);
|
|
125
|
+
(0, globals_1.expect)(actualItems.length).toBe(safeItemCount);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Final test to verify the fix for issue #24
|
|
3
|
+
// Simulates the exact scenario: context_get with sessionId: "current" and includeMetadata: true
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
const database_1 = require("../../utils/database");
|
|
39
|
+
const RepositoryManager_1 = require("../../repositories/RepositoryManager");
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
// Mock functions from index.ts
|
|
44
|
+
function estimateTokens(text) {
|
|
45
|
+
return Math.ceil(text.length / 4);
|
|
46
|
+
}
|
|
47
|
+
function calculateSize(value) {
|
|
48
|
+
return Buffer.byteLength(value, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
function validatePaginationParams(params) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
let limit = 25; // default
|
|
53
|
+
let offset = 0; // default
|
|
54
|
+
// Validate limit
|
|
55
|
+
if (params.limit !== undefined && params.limit !== null) {
|
|
56
|
+
const rawLimit = params.limit;
|
|
57
|
+
if (!Number.isInteger(rawLimit) || rawLimit <= 0) {
|
|
58
|
+
errors.push(`Invalid limit: expected positive integer, got ${typeof rawLimit} '${rawLimit}'`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
limit = Math.min(Math.max(1, rawLimit), 100); // clamp between 1-100
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Validate offset
|
|
65
|
+
if (params.offset !== undefined && params.offset !== null) {
|
|
66
|
+
const rawOffset = params.offset;
|
|
67
|
+
if (!Number.isInteger(rawOffset) || rawOffset < 0) {
|
|
68
|
+
errors.push(`Invalid offset: expected non-negative integer, got ${typeof rawOffset} '${rawOffset}'`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
offset = rawOffset;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { limit, offset, errors };
|
|
75
|
+
}
|
|
76
|
+
describe('Issue #24 - Final Fix Verification', () => {
|
|
77
|
+
let dbManager;
|
|
78
|
+
let repositories;
|
|
79
|
+
let tempDbPath;
|
|
80
|
+
let testSessionId;
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
tempDbPath = path.join(os.tmpdir(), `test-issue24-final-${Date.now()}.db`);
|
|
83
|
+
dbManager = new database_1.DatabaseManager({
|
|
84
|
+
filename: tempDbPath,
|
|
85
|
+
maxSize: 10 * 1024 * 1024,
|
|
86
|
+
walMode: true,
|
|
87
|
+
});
|
|
88
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
89
|
+
// Create test session (simulating "current" session)
|
|
90
|
+
const session = repositories.sessions.create({
|
|
91
|
+
name: 'Current Session',
|
|
92
|
+
description: 'Simulating the current active session',
|
|
93
|
+
});
|
|
94
|
+
testSessionId = session.id;
|
|
95
|
+
// Create many realistic items that would cause overflow
|
|
96
|
+
const largeContent = `
|
|
97
|
+
This is a realistic context item saved during development.
|
|
98
|
+
It contains implementation details, code snippets, notes, and documentation.
|
|
99
|
+
The content is substantial to simulate real-world usage patterns.
|
|
100
|
+
Developers often save detailed context about complex features, debugging sessions,
|
|
101
|
+
architectural decisions, API documentation, and troubleshooting information.
|
|
102
|
+
`.trim();
|
|
103
|
+
for (let i = 0; i < 200; i++) {
|
|
104
|
+
repositories.contexts.save(testSessionId, {
|
|
105
|
+
key: `context_${String(i).padStart(3, '0')}`,
|
|
106
|
+
value: `${largeContent}\n\nSpecific item ${i} notes and details.`,
|
|
107
|
+
category: ['task', 'decision', 'progress', 'note'][i % 4],
|
|
108
|
+
priority: ['high', 'normal', 'low'][i % 3],
|
|
109
|
+
channel: `channel-${i % 10}`,
|
|
110
|
+
metadata: JSON.stringify({
|
|
111
|
+
index: i,
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
tags: ['dev', 'test', 'review'][i % 3],
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
dbManager.close();
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(tempDbPath);
|
|
122
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
123
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
124
|
+
}
|
|
125
|
+
catch (_e) {
|
|
126
|
+
// Ignore
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
it('should handle context_get with includeMetadata: true without exceeding token limit', () => {
|
|
130
|
+
// Simulate the exact call from the issue:
|
|
131
|
+
// context_get(sessionId: "current", includeMetadata: true)
|
|
132
|
+
const args = {
|
|
133
|
+
sessionId: testSessionId, // Would be resolved from "current"
|
|
134
|
+
includeMetadata: true,
|
|
135
|
+
// No limit specified - should use new default of 30
|
|
136
|
+
};
|
|
137
|
+
// Simulate what the handler does
|
|
138
|
+
const includeMetadata = args.includeMetadata;
|
|
139
|
+
const rawLimit = undefined; // Not specified in the call
|
|
140
|
+
const rawOffset = undefined;
|
|
141
|
+
// Apply our fix: default limit is 30 when includeMetadata is true
|
|
142
|
+
const defaultLimit = includeMetadata ? 30 : 100;
|
|
143
|
+
const paginationValidation = validatePaginationParams({
|
|
144
|
+
limit: rawLimit !== undefined ? rawLimit : defaultLimit,
|
|
145
|
+
offset: rawOffset,
|
|
146
|
+
});
|
|
147
|
+
const { limit, offset } = paginationValidation;
|
|
148
|
+
console.log(`Using limit: ${limit} (includeMetadata: ${includeMetadata})`);
|
|
149
|
+
// Query with the calculated limit
|
|
150
|
+
const result = repositories.contexts.queryEnhanced({
|
|
151
|
+
sessionId: testSessionId,
|
|
152
|
+
includeMetadata: true,
|
|
153
|
+
limit: limit,
|
|
154
|
+
offset: offset,
|
|
155
|
+
});
|
|
156
|
+
console.log(`Retrieved ${result.items.length} of ${result.totalCount} items`);
|
|
157
|
+
// Transform items with metadata
|
|
158
|
+
const itemsWithMetadata = result.items.map(item => ({
|
|
159
|
+
key: item.key,
|
|
160
|
+
value: item.value,
|
|
161
|
+
category: item.category,
|
|
162
|
+
priority: item.priority,
|
|
163
|
+
channel: item.channel,
|
|
164
|
+
metadata: item.metadata ? JSON.parse(item.metadata) : null,
|
|
165
|
+
size: item.size || calculateSize(item.value),
|
|
166
|
+
created_at: item.created_at,
|
|
167
|
+
updated_at: item.updated_at,
|
|
168
|
+
}));
|
|
169
|
+
// Calculate metrics for token checking
|
|
170
|
+
const itemsForMetrics = itemsWithMetadata;
|
|
171
|
+
const jsonString = JSON.stringify(itemsForMetrics);
|
|
172
|
+
const estimatedTokens = estimateTokens(jsonString);
|
|
173
|
+
console.log(`Items token estimate: ${estimatedTokens}`);
|
|
174
|
+
// Build full response
|
|
175
|
+
const response = {
|
|
176
|
+
items: itemsWithMetadata,
|
|
177
|
+
pagination: {
|
|
178
|
+
total: result.totalCount,
|
|
179
|
+
returned: result.items.length,
|
|
180
|
+
offset: 0,
|
|
181
|
+
hasMore: result.totalCount > result.items.length,
|
|
182
|
+
nextOffset: result.items.length < result.totalCount ? result.items.length : null,
|
|
183
|
+
totalCount: result.totalCount,
|
|
184
|
+
page: 1,
|
|
185
|
+
pageSize: limit,
|
|
186
|
+
totalPages: Math.ceil(result.totalCount / limit),
|
|
187
|
+
hasNextPage: result.totalCount > result.items.length,
|
|
188
|
+
hasPreviousPage: false,
|
|
189
|
+
previousOffset: null,
|
|
190
|
+
totalSize: itemsWithMetadata.reduce((sum, item) => sum + (item.size || 0), 0),
|
|
191
|
+
averageSize: Math.round(itemsWithMetadata.reduce((sum, item) => sum + (item.size || 0), 0) / result.items.length),
|
|
192
|
+
defaultsApplied: { limit: true, sort: true },
|
|
193
|
+
truncated: false,
|
|
194
|
+
truncatedCount: 0,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const fullResponseJson = JSON.stringify(response, null, 2);
|
|
198
|
+
const finalTokens = estimateTokens(fullResponseJson);
|
|
199
|
+
console.log(`Final response tokens: ${finalTokens}`);
|
|
200
|
+
// Verify the fix works
|
|
201
|
+
expect(limit).toBe(30); // Our new default when includeMetadata is true
|
|
202
|
+
expect(result.items.length).toBeLessThanOrEqual(30);
|
|
203
|
+
expect(finalTokens).toBeLessThan(15000); // Our new conservative TOKEN_LIMIT
|
|
204
|
+
expect(finalTokens).toBeLessThan(25000); // MCP's actual limit
|
|
205
|
+
console.log('✅ Fix verified: Response stays well under token limit');
|
|
206
|
+
});
|
|
207
|
+
it('should still allow explicit higher limits but truncate if needed', () => {
|
|
208
|
+
// User explicitly requests more items
|
|
209
|
+
const args = {
|
|
210
|
+
sessionId: testSessionId,
|
|
211
|
+
includeMetadata: true,
|
|
212
|
+
limit: 100, // Explicitly requesting many items
|
|
213
|
+
};
|
|
214
|
+
const result = repositories.contexts.queryEnhanced({
|
|
215
|
+
sessionId: testSessionId,
|
|
216
|
+
includeMetadata: true,
|
|
217
|
+
limit: args.limit,
|
|
218
|
+
});
|
|
219
|
+
// With explicit limit, we get what was requested (up to 100)
|
|
220
|
+
expect(result.items.length).toBeLessThanOrEqual(100);
|
|
221
|
+
// But in the handler, if this exceeds tokens, it would be truncated
|
|
222
|
+
const itemsWithMetadata = result.items.map(item => ({
|
|
223
|
+
key: item.key,
|
|
224
|
+
value: item.value,
|
|
225
|
+
category: item.category,
|
|
226
|
+
priority: item.priority,
|
|
227
|
+
channel: item.channel,
|
|
228
|
+
metadata: item.metadata ? JSON.parse(item.metadata) : null,
|
|
229
|
+
size: item.size || calculateSize(item.value),
|
|
230
|
+
created_at: item.created_at,
|
|
231
|
+
updated_at: item.updated_at,
|
|
232
|
+
}));
|
|
233
|
+
const responseJson = JSON.stringify({ items: itemsWithMetadata }, null, 2);
|
|
234
|
+
const tokens = estimateTokens(responseJson);
|
|
235
|
+
console.log(`With explicit limit=100: ${result.items.length} items, ${tokens} tokens`);
|
|
236
|
+
// This would trigger truncation in the handler
|
|
237
|
+
if (tokens > 15000) {
|
|
238
|
+
console.log('⚠️ Would trigger truncation in handler');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|