Skip to content

Commit 24da165

Browse files
ArthurDEV44claude
andcommitted
feat(mcp-server): complete Phase 6 Advanced Compression
Phase 6.1 - Multi-File Context Compression: - Add multifile.ts compressor with cross-file deduplication - Extract shared imports, types, and constants across files - Smart chunking for large codebases with token budget - ctx.multifile SDK module (compress, extractShared, chunk, skeletons) - multifile_compress MCP tool Phase 6.2 - Conversation Memory Compression: - Extend conversation.ts with memory management - Decision extraction with pattern matching - Code reference tracking (created, modified, discussed) - Context restoration from compressed state - ctx.conversation SDK module (createMemory, extractDecisions, restore) - conversation_memory MCP tool Phase 6.3 - Output Format Optimization: - Global output configuration (verbosity, mode, TOON support) - Extended toon-serializer with generic result serialization - set_output_config MCP tool for runtime configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f52b63b commit 24da165

File tree

14 files changed

+2900
-15
lines changed

14 files changed

+2900
-15
lines changed

ROADMAP.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,25 +237,27 @@ session_stats()
237237
238238
## Phase 6: Advanced Compression
239239
240+
**Status: Complete**
241+
240242
**Goal**: Push compression boundaries with advanced techniques.
241243
242244
### 6.1 Multi-File Context Compression
243245
244-
- [ ] Cross-file deduplication (shared imports, types)
245-
- [ ] Dependency-aware skeleton extraction
246-
- [ ] Smart chunking for large codebases
246+
- [x] Cross-file deduplication (shared imports, types)
247+
- [x] Dependency-aware skeleton extraction
248+
- [x] Smart chunking for large codebases
247249
248250
### 6.2 Conversation Memory Compression
249251
250-
- [ ] Long conversation summarization
251-
- [ ] Key decision extraction
252-
- [ ] Context restoration from compressed state
252+
- [x] Long conversation summarization
253+
- [x] Key decision extraction
254+
- [x] Context restoration from compressed state
253255
254256
### 6.3 Output Format Optimization
255257
256-
- [ ] TOON format for all tool outputs
257-
- [ ] Configurable verbosity levels
258-
- [ ] Structured vs prose output modes
258+
- [x] TOON format for all tool outputs
259+
- [x] Configurable verbosity levels
260+
- [x] Structured vs prose output modes
259261
260262
---
261263
@@ -315,12 +317,17 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
315317
- Lazy MCP pattern
316318
- `code_execute` SDK
317319
318-
### Next Release
319-
- Extended SDK (`ctx.search.*`, `ctx.analyze.*`)
320-
- Composable pipelines
321-
- Improved session analytics
322-
323-
### v0.5.0 (Current)
320+
### v0.6.0 (Current)
321+
- Phase 6: Advanced Compression
322+
- `multifile_compress` tool: cross-file deduplication, smart chunking
323+
- `conversation_memory` tool: decision extraction, context restoration
324+
- `set_output_config` tool: global verbosity and format settings
325+
- `ctx.multifile` SDK module: compress, extractShared, chunk, skeletons
326+
- `ctx.conversation` SDK module: createMemory, extractDecisions, restore
327+
- TOON format support extended for generic result serialization
328+
- Enhanced conversation compressor with memory management
329+
330+
### v0.5.0
324331
- Phase 5: Ecosystem Integration
325332
- GitHub Action for token usage analysis (`action/action.yml`)
326333
- Pre-commit hook for large file warnings (`scripts/pre-commit-hook.sh`)

packages/mcp-server/src/compressors/conversation.ts

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,305 @@ export function compressConversation(
289289
savings: Math.max(0, savings), // Ensure non-negative
290290
};
291291
}
292+
293+
// ============================================
294+
// Memory Management (Phase 6.2)
295+
// ============================================
296+
297+
/**
298+
* Decision extracted from conversation
299+
*/
300+
export interface Decision {
301+
decision: string;
302+
context: string;
303+
timestamp: number;
304+
}
305+
306+
/**
307+
* Code reference from conversation
308+
*/
309+
export interface CodeReference {
310+
file: string;
311+
element?: string;
312+
action: "created" | "modified" | "discussed" | "deleted";
313+
}
314+
315+
/**
316+
* Conversation memory state
317+
*/
318+
export interface ConversationMemory {
319+
/** Rolling summary of conversation */
320+
summary: string;
321+
/** Key decisions made */
322+
decisions: Decision[];
323+
/** Code references mentioned */
324+
codeReferences: CodeReference[];
325+
/** Compressed message history */
326+
compressedHistory: ConversationMessage[];
327+
/** Timestamp of last update */
328+
lastUpdated: number;
329+
}
330+
331+
/**
332+
* Options for memory restoration
333+
*/
334+
export interface MemoryRestoreOptions {
335+
/** Include full summary */
336+
includeSummary?: boolean;
337+
/** Number of recent messages to include */
338+
recentMessages?: number;
339+
/** Include code references */
340+
includeCodeRefs?: boolean;
341+
/** Include decisions */
342+
includeDecisions?: boolean;
343+
}
344+
345+
/**
346+
* Result of conversation memory operation
347+
*/
348+
export interface ConversationMemoryResult {
349+
/** Restored context string */
350+
context: string;
351+
/** Memory state */
352+
memory: ConversationMemory;
353+
/** Statistics */
354+
stats: {
355+
originalTokens: number;
356+
compressedTokens: number;
357+
decisionsExtracted: number;
358+
codeRefsFound: number;
359+
};
360+
}
361+
362+
/**
363+
* Extract decisions from messages with enhanced patterns
364+
*/
365+
export function extractDecisions(messages: ConversationMessage[]): Decision[] {
366+
const decisions: Decision[] = [];
367+
const now = Date.now();
368+
369+
// Decision patterns
370+
const decisionPatterns = [
371+
// Direct decisions
372+
/(?:decided|will|going to|let's|I'll|we'll|we should)\s+(.{10,150})/gi,
373+
// Plans and approaches
374+
/(?:the approach|the solution|the plan|strategy)\s+(?:is|will be)\s+(.{10,150})/gi,
375+
// Requirements
376+
/(?:we need to|must|should|have to)\s+(.{10,150})/gi,
377+
// Completed actions
378+
/(?:done|completed|finished|implemented|created|fixed|updated):\s*(.{10,150})/gi,
379+
];
380+
381+
for (const msg of messages) {
382+
if (msg.role === "system") continue;
383+
384+
const lines = msg.content.split("\n");
385+
386+
for (const line of lines) {
387+
const trimmed = line.trim();
388+
if (trimmed.length < 15 || trimmed.length > 300) continue;
389+
390+
for (const pattern of decisionPatterns) {
391+
pattern.lastIndex = 0; // Reset regex state
392+
const match = pattern.exec(trimmed);
393+
if (match && match[1]) {
394+
const decision = match[1].trim();
395+
// Avoid duplicates
396+
if (!decisions.some((d) => d.decision === decision)) {
397+
decisions.push({
398+
decision,
399+
context: trimmed.slice(0, 200),
400+
timestamp: now,
401+
});
402+
}
403+
}
404+
}
405+
406+
// Also check for numbered list items that look like decisions
407+
if (/^\d+\.\s+/.test(trimmed)) {
408+
const item = trimmed.replace(/^\d+\.\s+/, "").trim();
409+
if (
410+
item.length > 15 &&
411+
item.length < 200 &&
412+
!decisions.some((d) => d.decision === item)
413+
) {
414+
decisions.push({
415+
decision: item,
416+
context: trimmed,
417+
timestamp: now,
418+
});
419+
}
420+
}
421+
}
422+
}
423+
424+
// Limit to 20 most recent decisions
425+
return decisions.slice(-20);
426+
}
427+
428+
/**
429+
* Extract code references from messages
430+
*/
431+
export function extractCodeReferences(
432+
messages: ConversationMessage[]
433+
): CodeReference[] {
434+
const refs: CodeReference[] = [];
435+
const seenFiles = new Set<string>();
436+
437+
// File path pattern
438+
const filePattern =
439+
/[a-zA-Z0-9_\-./]+\.(ts|js|tsx|jsx|py|go|rs|json|yaml|yml|md|css|scss|html)\b/g;
440+
441+
// Action patterns
442+
const actionPatterns: Array<{ pattern: RegExp; action: CodeReference["action"] }> = [
443+
{ pattern: /\b(created?|add(?:ed|ing)?|writ(?:e|ing|ten))\b/i, action: "created" },
444+
{ pattern: /\b(modif(?:y|ied|ying)|updat(?:e|ed|ing)|chang(?:e|ed|ing)|edit(?:ed|ing)?)\b/i, action: "modified" },
445+
{ pattern: /\b(delet(?:e|ed|ing)|remov(?:e|ed|ing))\b/i, action: "deleted" },
446+
];
447+
448+
for (const msg of messages) {
449+
const content = msg.content;
450+
let match;
451+
452+
while ((match = filePattern.exec(content)) !== null) {
453+
const file = match[0];
454+
if (seenFiles.has(file)) continue;
455+
seenFiles.add(file);
456+
457+
// Find surrounding context to determine action
458+
const start = Math.max(0, match.index - 50);
459+
const end = Math.min(content.length, match.index + file.length + 50);
460+
const context = content.slice(start, end).toLowerCase();
461+
462+
let action: CodeReference["action"] = "discussed";
463+
for (const { pattern, action: act } of actionPatterns) {
464+
if (pattern.test(context)) {
465+
action = act;
466+
break;
467+
}
468+
}
469+
470+
refs.push({ file, action });
471+
}
472+
}
473+
474+
// Limit to 30 most relevant
475+
return refs.slice(0, 30);
476+
}
477+
478+
/**
479+
* Create conversation memory from messages
480+
*/
481+
export function createMemory(
482+
messages: ConversationMessage[],
483+
options: ConversationCompressOptions
484+
): ConversationMemory {
485+
const summary = createRollingSummary(messages);
486+
const decisions = extractDecisions(messages);
487+
const codeReferences = extractCodeReferences(messages);
488+
489+
// Compress messages using hybrid strategy
490+
const result = compressConversation(messages, {
491+
...options,
492+
strategy: "hybrid",
493+
});
494+
495+
return {
496+
summary,
497+
decisions,
498+
codeReferences,
499+
compressedHistory: result.compressedMessages,
500+
lastUpdated: Date.now(),
501+
};
502+
}
503+
504+
/**
505+
* Restore context from memory state
506+
*/
507+
export function restoreContext(
508+
memory: ConversationMemory,
509+
options: MemoryRestoreOptions = {}
510+
): string {
511+
const {
512+
includeSummary = true,
513+
recentMessages = 3,
514+
includeCodeRefs = true,
515+
includeDecisions = true,
516+
} = options;
517+
518+
const parts: string[] = [];
519+
520+
// Add summary
521+
if (includeSummary && memory.summary) {
522+
parts.push("[Previous Context]");
523+
parts.push(memory.summary);
524+
parts.push("");
525+
}
526+
527+
// Add decisions
528+
if (includeDecisions && memory.decisions.length > 0) {
529+
parts.push("[Key Decisions]");
530+
for (const decision of memory.decisions.slice(-10)) {
531+
parts.push(`- ${decision.decision}`);
532+
}
533+
parts.push("");
534+
}
535+
536+
// Add code references
537+
if (includeCodeRefs && memory.codeReferences.length > 0) {
538+
parts.push("[Code References]");
539+
const byAction = new Map<string, string[]>();
540+
541+
for (const ref of memory.codeReferences) {
542+
if (!byAction.has(ref.action)) {
543+
byAction.set(ref.action, []);
544+
}
545+
byAction.get(ref.action)!.push(ref.file);
546+
}
547+
548+
for (const [action, files] of byAction) {
549+
parts.push(`${action}: ${files.slice(0, 10).join(", ")}`);
550+
}
551+
parts.push("");
552+
}
553+
554+
// Add recent messages
555+
if (recentMessages > 0 && memory.compressedHistory.length > 0) {
556+
parts.push("[Recent Messages]");
557+
const recent = memory.compressedHistory.slice(-recentMessages);
558+
for (const msg of recent) {
559+
const preview = msg.content.slice(0, 200);
560+
parts.push(`${msg.role}: ${preview}${msg.content.length > 200 ? "..." : ""}`);
561+
}
562+
}
563+
564+
return parts.join("\n");
565+
}
566+
567+
/**
568+
* Compress conversation and create memory result
569+
*/
570+
export function compressConversationWithMemory(
571+
messages: ConversationMessage[],
572+
options: ConversationCompressOptions
573+
): ConversationMemoryResult {
574+
const originalTokens = messages.reduce(
575+
(sum, m) => sum + countTokens(m.content),
576+
0
577+
);
578+
579+
const memory = createMemory(messages, options);
580+
const context = restoreContext(memory);
581+
const compressedTokens = countTokens(context);
582+
583+
return {
584+
context,
585+
memory,
586+
stats: {
587+
originalTokens,
588+
compressedTokens,
589+
decisionsExtracted: memory.decisions.length,
590+
codeRefsFound: memory.codeReferences.length,
591+
},
592+
};
593+
}

0 commit comments

Comments
 (0)