@claudetools/tools 0.8.2 → 0.8.4

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.
Files changed (74) hide show
  1. package/dist/cli.js +41 -0
  2. package/dist/context/deduplication.d.ts +72 -0
  3. package/dist/context/deduplication.js +77 -0
  4. package/dist/context/deduplication.test.d.ts +6 -0
  5. package/dist/context/deduplication.test.js +84 -0
  6. package/dist/context/emergency-eviction.d.ts +73 -0
  7. package/dist/context/emergency-eviction.example.d.ts +13 -0
  8. package/dist/context/emergency-eviction.example.js +94 -0
  9. package/dist/context/emergency-eviction.js +226 -0
  10. package/dist/context/eviction-engine.d.ts +76 -0
  11. package/dist/context/eviction-engine.example.d.ts +7 -0
  12. package/dist/context/eviction-engine.example.js +144 -0
  13. package/dist/context/eviction-engine.js +176 -0
  14. package/dist/context/example-usage.d.ts +1 -0
  15. package/dist/context/example-usage.js +128 -0
  16. package/dist/context/exchange-summariser.d.ts +80 -0
  17. package/dist/context/exchange-summariser.js +261 -0
  18. package/dist/context/health-monitor.d.ts +97 -0
  19. package/dist/context/health-monitor.example.d.ts +1 -0
  20. package/dist/context/health-monitor.example.js +164 -0
  21. package/dist/context/health-monitor.js +210 -0
  22. package/dist/context/importance-scorer.d.ts +94 -0
  23. package/dist/context/importance-scorer.example.d.ts +1 -0
  24. package/dist/context/importance-scorer.example.js +140 -0
  25. package/dist/context/importance-scorer.js +187 -0
  26. package/dist/context/index.d.ts +9 -0
  27. package/dist/context/index.js +16 -0
  28. package/dist/context/session-helper.d.ts +10 -0
  29. package/dist/context/session-helper.js +51 -0
  30. package/dist/context/session-store.d.ts +94 -0
  31. package/dist/context/session-store.js +286 -0
  32. package/dist/context/usage-estimator.d.ts +131 -0
  33. package/dist/context/usage-estimator.js +260 -0
  34. package/dist/context/usage-estimator.test.d.ts +1 -0
  35. package/dist/context/usage-estimator.test.js +208 -0
  36. package/dist/context-cli.d.ts +16 -0
  37. package/dist/context-cli.js +309 -0
  38. package/dist/evaluation/build-dataset.d.ts +1 -0
  39. package/dist/evaluation/build-dataset.js +135 -0
  40. package/dist/evaluation/threshold-eval.d.ts +63 -0
  41. package/dist/evaluation/threshold-eval.js +250 -0
  42. package/dist/handlers/codedna-handlers.d.ts +2 -2
  43. package/dist/handlers/tool-handlers.js +126 -165
  44. package/dist/helpers/api-client.d.ts +5 -1
  45. package/dist/helpers/api-client.js +3 -1
  46. package/dist/helpers/compact-formatter.d.ts +51 -0
  47. package/dist/helpers/compact-formatter.js +130 -0
  48. package/dist/helpers/engagement-tracker.d.ts +10 -0
  49. package/dist/helpers/engagement-tracker.js +61 -0
  50. package/dist/helpers/error-tracking.js +1 -1
  51. package/dist/helpers/session-validation.d.ts +76 -0
  52. package/dist/helpers/session-validation.js +221 -0
  53. package/dist/helpers/usage-analytics.js +1 -1
  54. package/dist/hooks/index.d.ts +4 -0
  55. package/dist/hooks/index.js +6 -0
  56. package/dist/hooks/post-tool-use-hook-cli.d.ts +2 -0
  57. package/dist/hooks/post-tool-use-hook-cli.js +34 -0
  58. package/dist/hooks/post-tool-use.d.ts +67 -0
  59. package/dist/hooks/post-tool-use.js +234 -0
  60. package/dist/hooks/stop-hook-cli.d.ts +2 -0
  61. package/dist/hooks/stop-hook-cli.js +34 -0
  62. package/dist/hooks/stop.d.ts +64 -0
  63. package/dist/hooks/stop.js +192 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.js +2 -0
  66. package/dist/logger.d.ts +1 -1
  67. package/dist/logger.js +4 -0
  68. package/dist/resources.js +3 -0
  69. package/dist/setup.js +206 -2
  70. package/dist/templates/claude-md.d.ts +1 -1
  71. package/dist/templates/claude-md.js +23 -35
  72. package/dist/templates/worker-prompt.js +35 -202
  73. package/dist/tools.js +26 -20
  74. package/package.json +6 -2
@@ -0,0 +1,226 @@
1
+ // =============================================================================
2
+ // Emergency Context Eviction
3
+ // =============================================================================
4
+ // Triggered at 70% context fill - aggressive context reduction protocol
5
+ //
6
+ // Emergency Protocol:
7
+ // 1. Immediately summarise last 5 exchanges (if not already summarised)
8
+ // 2. Evict ALL normal importance facts
9
+ // 3. If still > 65%, compress critical facts to essential summaries
10
+ // 4. Log warning to user about context pressure
11
+ // 5. Return to safe fill level (< 50%)
12
+ //
13
+ // This is the last-resort mechanism before context overflow.
14
+ // =============================================================================
15
+ import { ImportanceScorer } from './importance-scorer.js';
16
+ import { mcpLogger } from '../logger.js';
17
+ // -----------------------------------------------------------------------------
18
+ // Constants
19
+ // -----------------------------------------------------------------------------
20
+ const DEFAULT_CONFIG = {
21
+ emergencyThreshold: 0.70,
22
+ targetFill: 0.50,
23
+ criticalCompressionThreshold: 0.65,
24
+ };
25
+ // Emergency thresholds for importance levels
26
+ const IMPORTANCE_THRESHOLDS = {
27
+ critical: 1.0, // Critical facts have base importance 1.0
28
+ high: 0.7, // High importance facts have base 0.7
29
+ normal: 0.4, // Normal importance facts have base 0.4
30
+ };
31
+ // -----------------------------------------------------------------------------
32
+ // Emergency Eviction Engine
33
+ // -----------------------------------------------------------------------------
34
+ export class EmergencyEviction {
35
+ config;
36
+ scorer;
37
+ constructor(config = {}) {
38
+ this.config = { ...DEFAULT_CONFIG, ...config };
39
+ this.scorer = new ImportanceScorer();
40
+ }
41
+ /**
42
+ * Check if session is in emergency state
43
+ */
44
+ isEmergency(session) {
45
+ return session.estimated_fill >= this.config.emergencyThreshold;
46
+ }
47
+ /**
48
+ * Run emergency eviction protocol
49
+ *
50
+ * Steps:
51
+ * 1. Summarise recent exchanges
52
+ * 2. Evict normal importance facts
53
+ * 3. If still critical, compress high importance facts
54
+ * 4. If still critical, compress critical facts (last resort)
55
+ *
56
+ * @param session - Current session state
57
+ * @returns Emergency result with actions taken
58
+ */
59
+ async runEmergencyEviction(session) {
60
+ const actionsToken = [];
61
+ let evictedCount = 0;
62
+ let summarisedExchanges = 0;
63
+ let compressedCritical = 0;
64
+ let currentFill = session.estimated_fill;
65
+ // Check if emergency
66
+ if (!this.isEmergency(session)) {
67
+ return {
68
+ wasEmergency: false,
69
+ actionsToken: ['No emergency - fill below threshold'],
70
+ evictedCount: 0,
71
+ summarisedExchanges: 0,
72
+ compressedCritical: 0,
73
+ newEstimatedFill: currentFill,
74
+ userWarning: '',
75
+ };
76
+ }
77
+ mcpLogger.warn('MEMORY', `🚨 EMERGENCY EVICTION: Context at ${(currentFill * 100).toFixed(1)}%`);
78
+ // Step 1: Summarise recent exchanges (if not already summarised)
79
+ const unsummarisedCount = this.getUnsummarisedExchangeCount(session);
80
+ if (unsummarisedCount > 0) {
81
+ const toSummarise = Math.min(5, unsummarisedCount);
82
+ summarisedExchanges = toSummarise;
83
+ // Note: Actual summarisation happens via ExchangeSummariser
84
+ // Here we just track what WOULD be summarised
85
+ actionsToken.push(`Summarised ${toSummarise} exchanges`);
86
+ // Estimate token savings from summarisation (rough: 70% reduction)
87
+ const savedTokens = this.estimateExchangeTokens(session, toSummarise) * 0.7;
88
+ currentFill = this.recalculateFill(session, currentFill, -savedTokens);
89
+ mcpLogger.info('MEMORY', `Summarised ${toSummarise} exchanges, new fill: ${(currentFill * 100).toFixed(1)}%`);
90
+ }
91
+ // Step 2: Evict ALL normal importance facts
92
+ if (currentFill > this.config.targetFill) {
93
+ const normalFacts = this.getFactsByImportance(session, 'normal');
94
+ evictedCount += normalFacts.length;
95
+ if (normalFacts.length > 0) {
96
+ actionsToken.push(`Evicted ${normalFacts.length} normal importance facts`);
97
+ const savedTokens = this.estimateFactTokens(normalFacts);
98
+ currentFill = this.recalculateFill(session, currentFill, -savedTokens);
99
+ mcpLogger.info('MEMORY', `Evicted ${normalFacts.length} normal facts, new fill: ${(currentFill * 100).toFixed(1)}%`);
100
+ }
101
+ }
102
+ // Step 3: If still critical, evict high importance facts
103
+ if (currentFill > this.config.criticalCompressionThreshold) {
104
+ const highFacts = this.getFactsByImportance(session, 'high');
105
+ evictedCount += highFacts.length;
106
+ if (highFacts.length > 0) {
107
+ actionsToken.push(`Evicted ${highFacts.length} high importance facts`);
108
+ const savedTokens = this.estimateFactTokens(highFacts);
109
+ currentFill = this.recalculateFill(session, currentFill, -savedTokens);
110
+ mcpLogger.warn('MEMORY', `⚠️ Evicted ${highFacts.length} high importance facts, new fill: ${(currentFill * 100).toFixed(1)}%`);
111
+ }
112
+ }
113
+ // Step 4: Last resort - compress critical facts
114
+ if (currentFill > this.config.criticalCompressionThreshold) {
115
+ const criticalFacts = this.getFactsByImportance(session, 'critical');
116
+ if (criticalFacts.length > 0) {
117
+ // Compress to essential summaries (keep 30% of tokens)
118
+ compressedCritical = criticalFacts.length;
119
+ actionsToken.push(`Compressed ${criticalFacts.length} critical facts to summaries`);
120
+ const originalTokens = this.estimateFactTokens(criticalFacts);
121
+ const savedTokens = originalTokens * 0.7; // Save 70% via compression
122
+ currentFill = this.recalculateFill(session, currentFill, -savedTokens);
123
+ mcpLogger.error('MEMORY', `🔥 CRITICAL: Compressed ${criticalFacts.length} critical facts, new fill: ${(currentFill * 100).toFixed(1)}%`);
124
+ }
125
+ }
126
+ // Generate user warning
127
+ const userWarning = this.generateUserWarning(session.estimated_fill, currentFill, evictedCount, summarisedExchanges, compressedCritical);
128
+ return {
129
+ wasEmergency: true,
130
+ actionsToken,
131
+ evictedCount,
132
+ summarisedExchanges,
133
+ compressedCritical,
134
+ newEstimatedFill: currentFill,
135
+ userWarning,
136
+ };
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Private Helpers
140
+ // ---------------------------------------------------------------------------
141
+ /**
142
+ * Get facts by importance level
143
+ */
144
+ getFactsByImportance(session, level) {
145
+ const threshold = IMPORTANCE_THRESHOLDS[level];
146
+ return session.injected_facts.filter((fact) => {
147
+ const scored = this.scorer.calculateWithBreakdown(fact, session);
148
+ // Match facts at this exact importance tier
149
+ if (level === 'normal') {
150
+ return scored.breakdown.base === IMPORTANCE_THRESHOLDS.normal;
151
+ }
152
+ else if (level === 'high') {
153
+ return scored.breakdown.base === IMPORTANCE_THRESHOLDS.high;
154
+ }
155
+ else {
156
+ return scored.breakdown.base === IMPORTANCE_THRESHOLDS.critical;
157
+ }
158
+ });
159
+ }
160
+ /**
161
+ * Get count of unsummarised exchanges
162
+ */
163
+ getUnsummarisedExchangeCount(session) {
164
+ return session.exchanges.filter((e) => !e.summarised_at).length;
165
+ }
166
+ /**
167
+ * Estimate tokens for a set of exchanges
168
+ */
169
+ estimateExchangeTokens(session, count) {
170
+ // Get last N unsummarised exchanges
171
+ const unsummarised = session.exchanges
172
+ .filter((e) => !e.summarised_at)
173
+ .slice(-count);
174
+ return unsummarised.reduce((sum, e) => sum + e.user_tokens + e.assistant_tokens + e.tool_tokens, 0);
175
+ }
176
+ /**
177
+ * Estimate tokens for a set of facts
178
+ * More realistic estimate: ~500 tokens per fact average
179
+ * (includes context, entities, relationships, metadata)
180
+ */
181
+ estimateFactTokens(facts) {
182
+ // TODO: Use actual token estimation when available
183
+ return facts.length * 500;
184
+ }
185
+ /**
186
+ * Recalculate fill percentage after token change
187
+ */
188
+ recalculateFill(session, currentFill, tokenDelta) {
189
+ const currentTokens = currentFill * session.context_limit;
190
+ const newTokens = Math.max(0, currentTokens + tokenDelta);
191
+ return newTokens / session.context_limit;
192
+ }
193
+ /**
194
+ * Generate user-facing warning message
195
+ */
196
+ generateUserWarning(originalFill, newFill, evictedCount, summarisedExchanges, compressedCritical) {
197
+ const parts = [];
198
+ parts.push(`Context pressure high (${(originalFill * 100).toFixed(1)}% → ${(newFill * 100).toFixed(1)}%).`);
199
+ if (summarisedExchanges > 0) {
200
+ parts.push(`Summarised ${summarisedExchanges} exchanges.`);
201
+ }
202
+ if (evictedCount > 0) {
203
+ parts.push(`Evicted ${evictedCount} facts.`);
204
+ }
205
+ if (compressedCritical > 0) {
206
+ parts.push(`⚠️ CRITICAL: Compressed ${compressedCritical} important facts to summaries.`);
207
+ }
208
+ if (newFill > 0.6) {
209
+ parts.push('Context still high - consider ending session soon.');
210
+ }
211
+ return parts.join(' ');
212
+ }
213
+ }
214
+ // -----------------------------------------------------------------------------
215
+ // Factory
216
+ // -----------------------------------------------------------------------------
217
+ /**
218
+ * Create emergency eviction instance with optional config
219
+ */
220
+ export function createEmergencyEviction(config) {
221
+ return new EmergencyEviction(config);
222
+ }
223
+ // -----------------------------------------------------------------------------
224
+ // Exports
225
+ // -----------------------------------------------------------------------------
226
+ // Types are exported inline above via 'export interface'
@@ -0,0 +1,76 @@
1
+ import { ImportanceScorer, type InjectedFact } from './importance-scorer.js';
2
+ import type { PersistedSessionState } from './session-store.js';
3
+ /**
4
+ * Result of an eviction operation
5
+ */
6
+ export interface EvictionResult {
7
+ evictedCount: number;
8
+ evictedFacts: InjectedFact[];
9
+ newEstimatedFill: number;
10
+ summaryGenerated?: string;
11
+ }
12
+ /**
13
+ * Plan for eviction before execution
14
+ */
15
+ export interface EvictionPlan {
16
+ factsToEvict: Array<{
17
+ fact: InjectedFact;
18
+ score: number;
19
+ }>;
20
+ expectedFillAfter: number;
21
+ includesCritical: boolean;
22
+ }
23
+ export declare class EvictionEngine {
24
+ private scorer;
25
+ constructor(scorer?: ImportanceScorer);
26
+ /**
27
+ * Check if eviction should be triggered
28
+ *
29
+ * @param session - Current session state
30
+ * @returns True if eviction is needed
31
+ */
32
+ shouldEvict(session: PersistedSessionState): boolean;
33
+ /**
34
+ * Get an eviction plan without executing it
35
+ * Useful for preview/confirmation before eviction
36
+ *
37
+ * @param session - Current session state
38
+ * @returns Eviction plan with facts to evict and expected results
39
+ */
40
+ getEvictionPlan(session: PersistedSessionState): EvictionPlan;
41
+ /**
42
+ * Execute eviction on a session
43
+ * Removes lowest-scored facts to reduce estimated fill
44
+ *
45
+ * @param session - Current session state
46
+ * @returns Eviction result with details of what was evicted
47
+ */
48
+ runEviction(session: PersistedSessionState): Promise<EvictionResult>;
49
+ /**
50
+ * Get eviction statistics for a session
51
+ * Useful for monitoring and debugging
52
+ *
53
+ * @param session - Current session state
54
+ * @returns Statistics about eviction potential
55
+ */
56
+ getEvictionStats(session: PersistedSessionState): {
57
+ currentFill: number;
58
+ shouldEvict: boolean;
59
+ criticalFactCount: number;
60
+ normalFactCount: number;
61
+ averageScore: number;
62
+ lowestScore: number;
63
+ highestScore: number;
64
+ };
65
+ /**
66
+ * Check if a fact is marked as critical
67
+ */
68
+ private isCritical;
69
+ }
70
+ /**
71
+ * Create a new eviction engine instance
72
+ *
73
+ * @param scorer - Optional importance scorer (creates new one if not provided)
74
+ * @returns Eviction engine instance
75
+ */
76
+ export declare function createEvictionEngine(scorer?: ImportanceScorer): EvictionEngine;
@@ -0,0 +1,7 @@
1
+ declare function checkIfEvictionNeeded(sessionId: string): Promise<void>;
2
+ declare function previewEvictionPlan(sessionId: string): Promise<void>;
3
+ declare function executeEviction(sessionId: string): Promise<void>;
4
+ declare function automaticEvictionLoop(sessionId: string): Promise<void>;
5
+ declare function getEvictionStatistics(sessionId: string): Promise<void>;
6
+ declare function sessionLifecycleExample(): Promise<void>;
7
+ export { checkIfEvictionNeeded, previewEvictionPlan, executeEviction, automaticEvictionLoop, getEvictionStatistics, sessionLifecycleExample, };
@@ -0,0 +1,144 @@
1
+ // =============================================================================
2
+ // Eviction Engine Usage Examples
3
+ // =============================================================================
4
+ // This file demonstrates how to use the EvictionEngine for automatic context
5
+ // window management in ClaudeTools Memory.
6
+ // =============================================================================
7
+ import { createEvictionEngine, } from './eviction-engine.js';
8
+ import { getSessionStore } from './session-store.js';
9
+ // -----------------------------------------------------------------------------
10
+ // Example 1: Basic Eviction Check
11
+ // -----------------------------------------------------------------------------
12
+ async function checkIfEvictionNeeded(sessionId) {
13
+ const store = getSessionStore();
14
+ const session = await store.getSession(sessionId);
15
+ if (!session) {
16
+ console.log('Session not found');
17
+ return;
18
+ }
19
+ const engine = createEvictionEngine();
20
+ // Check if eviction should run
21
+ if (engine.shouldEvict(session)) {
22
+ console.log(`⚠️ Eviction needed (fill: ${(session.estimated_fill * 100).toFixed(1)}%)`);
23
+ }
24
+ else {
25
+ console.log(`✓ Context healthy (fill: ${(session.estimated_fill * 100).toFixed(1)}%)`);
26
+ }
27
+ }
28
+ // -----------------------------------------------------------------------------
29
+ // Example 2: Preview Eviction Plan
30
+ // -----------------------------------------------------------------------------
31
+ async function previewEvictionPlan(sessionId) {
32
+ const store = getSessionStore();
33
+ const session = await store.getSession(sessionId);
34
+ if (!session) {
35
+ console.log('Session not found');
36
+ return;
37
+ }
38
+ const engine = createEvictionEngine();
39
+ const plan = engine.getEvictionPlan(session);
40
+ console.log('Eviction Plan:');
41
+ console.log(` Facts to evict: ${plan.factsToEvict.length}`);
42
+ console.log(` Current fill: ${(session.estimated_fill * 100).toFixed(1)}%`);
43
+ console.log(` Expected fill after: ${(plan.expectedFillAfter * 100).toFixed(1)}%`);
44
+ console.log(` Includes critical facts: ${plan.includesCritical}`);
45
+ if (plan.factsToEvict.length > 0) {
46
+ console.log('\nFacts to evict:');
47
+ plan.factsToEvict.forEach(({ fact, score }, idx) => {
48
+ console.log(` ${idx + 1}. ${fact.edge_id} (score: ${score.toFixed(3)})`);
49
+ });
50
+ }
51
+ }
52
+ // -----------------------------------------------------------------------------
53
+ // Example 3: Execute Eviction
54
+ // -----------------------------------------------------------------------------
55
+ async function executeEviction(sessionId) {
56
+ const store = getSessionStore();
57
+ const session = await store.getSession(sessionId);
58
+ if (!session) {
59
+ console.log('Session not found');
60
+ return;
61
+ }
62
+ const engine = createEvictionEngine();
63
+ // Run eviction
64
+ const result = await engine.runEviction(session);
65
+ console.log('Eviction completed:');
66
+ console.log(` Evicted: ${result.evictedCount} facts`);
67
+ console.log(` New fill: ${(result.newEstimatedFill * 100).toFixed(1)}%`);
68
+ // Update session state (in real usage, you'd persist this)
69
+ // session.estimated_fill = result.newEstimatedFill;
70
+ // session.injected_facts = session.injected_facts.filter(
71
+ // f => !result.evictedFacts.find(ef => ef.edge_id === f.edge_id)
72
+ // );
73
+ // await store.updateSession(sessionId, { estimated_fill: result.newEstimatedFill });
74
+ }
75
+ // -----------------------------------------------------------------------------
76
+ // Example 4: Automatic Eviction Loop
77
+ // -----------------------------------------------------------------------------
78
+ async function automaticEvictionLoop(sessionId) {
79
+ const store = getSessionStore();
80
+ const engine = createEvictionEngine();
81
+ let session = await store.getSession(sessionId);
82
+ if (!session) {
83
+ console.log('Session not found');
84
+ return;
85
+ }
86
+ console.log(`Starting automatic eviction loop for session ${sessionId}`);
87
+ // Keep evicting until fill is below threshold
88
+ while (engine.shouldEvict(session)) {
89
+ console.log(`\nFill at ${(session.estimated_fill * 100).toFixed(1)}% - evicting...`);
90
+ const result = await engine.runEviction(session);
91
+ console.log(` Evicted ${result.evictedCount} facts`);
92
+ console.log(` New fill: ${(result.newEstimatedFill * 100).toFixed(1)}%`);
93
+ // Update session (in real usage, you'd persist to store)
94
+ session.estimated_fill = result.newEstimatedFill;
95
+ session.injected_facts = session.injected_facts.filter((f) => !result.evictedFacts.find((ef) => ef.edge_id === f.edge_id));
96
+ }
97
+ console.log('\n✓ Eviction complete - context healthy');
98
+ }
99
+ // -----------------------------------------------------------------------------
100
+ // Example 5: Get Eviction Statistics
101
+ // -----------------------------------------------------------------------------
102
+ async function getEvictionStatistics(sessionId) {
103
+ const store = getSessionStore();
104
+ const session = await store.getSession(sessionId);
105
+ if (!session) {
106
+ console.log('Session not found');
107
+ return;
108
+ }
109
+ const engine = createEvictionEngine();
110
+ const stats = engine.getEvictionStats(session);
111
+ console.log('Eviction Statistics:');
112
+ console.log(` Current fill: ${(stats.currentFill * 100).toFixed(1)}%`);
113
+ console.log(` Should evict: ${stats.shouldEvict ? 'YES' : 'NO'}`);
114
+ console.log(` Critical facts: ${stats.criticalFactCount}`);
115
+ console.log(` Normal facts: ${stats.normalFactCount}`);
116
+ console.log(` Average score: ${stats.averageScore.toFixed(3)}`);
117
+ console.log(` Score range: ${stats.lowestScore.toFixed(3)} - ${stats.highestScore.toFixed(3)}`);
118
+ }
119
+ // -----------------------------------------------------------------------------
120
+ // Example 6: Integration with Session Lifecycle
121
+ // -----------------------------------------------------------------------------
122
+ async function sessionLifecycleExample() {
123
+ const store = getSessionStore();
124
+ const engine = createEvictionEngine();
125
+ // Create a new session
126
+ const session = await store.createSession('example-session', 'sonnet');
127
+ console.log('Session created:', session.session_id);
128
+ // Simulate adding facts and tracking fill
129
+ // ... (facts would be added via store.addInjectedFact)
130
+ // At each turn, check if eviction is needed
131
+ if (engine.shouldEvict(session)) {
132
+ console.log('Running eviction...');
133
+ const result = await engine.runEviction(session);
134
+ // Update session state
135
+ await store.updateSession(session.session_id, {
136
+ estimated_fill: result.newEstimatedFill,
137
+ });
138
+ console.log(`Evicted ${result.evictedCount} facts`);
139
+ }
140
+ }
141
+ // -----------------------------------------------------------------------------
142
+ // Export examples for testing/demo purposes
143
+ // -----------------------------------------------------------------------------
144
+ export { checkIfEvictionNeeded, previewEvictionPlan, executeEviction, automaticEvictionLoop, getEvictionStatistics, sessionLifecycleExample, };
@@ -0,0 +1,176 @@
1
+ // =============================================================================
2
+ // ClaudeTools Memory - Eviction Engine
3
+ // =============================================================================
4
+ // Automatic fact eviction for context window management
5
+ //
6
+ // Triggers when estimated_fill > 0.60, targets reduction to < 0.50
7
+ // Evicts lowest-scored facts first, protects critical facts unless fill > 0.85
8
+ // =============================================================================
9
+ import { ImportanceScorer } from './importance-scorer.js';
10
+ import { mcpLogger } from '../logger.js';
11
+ // -----------------------------------------------------------------------------
12
+ // Configuration
13
+ // -----------------------------------------------------------------------------
14
+ const EVICTION_CONFIG = {
15
+ // Trigger eviction when estimated fill exceeds this threshold
16
+ TRIGGER_THRESHOLD: 0.60,
17
+ // Target fill level after eviction
18
+ TARGET_FILL: 0.50,
19
+ // Allow evicting critical facts only when fill is above this
20
+ CRITICAL_EVICTION_THRESHOLD: 0.85,
21
+ // Minimum number of facts to evict per operation (prevent thrashing)
22
+ MIN_EVICTION_COUNT: 3,
23
+ };
24
+ // -----------------------------------------------------------------------------
25
+ // Eviction Engine
26
+ // -----------------------------------------------------------------------------
27
+ export class EvictionEngine {
28
+ scorer;
29
+ constructor(scorer) {
30
+ this.scorer = scorer || new ImportanceScorer();
31
+ }
32
+ /**
33
+ * Check if eviction should be triggered
34
+ *
35
+ * @param session - Current session state
36
+ * @returns True if eviction is needed
37
+ */
38
+ shouldEvict(session) {
39
+ return session.estimated_fill > EVICTION_CONFIG.TRIGGER_THRESHOLD;
40
+ }
41
+ /**
42
+ * Get an eviction plan without executing it
43
+ * Useful for preview/confirmation before eviction
44
+ *
45
+ * @param session - Current session state
46
+ * @returns Eviction plan with facts to evict and expected results
47
+ */
48
+ getEvictionPlan(session) {
49
+ // Calculate how much we need to evict
50
+ const currentFill = session.estimated_fill;
51
+ const targetFill = EVICTION_CONFIG.TARGET_FILL;
52
+ const fillToReduce = currentFill - targetFill;
53
+ // Score all facts
54
+ const scoredFacts = this.scorer
55
+ .scoreAllFacts(session)
56
+ .sort((a, b) => a.score - b.score); // Lowest scores first
57
+ // Determine critical eviction threshold
58
+ const allowCriticalEviction = currentFill > EVICTION_CONFIG.CRITICAL_EVICTION_THRESHOLD;
59
+ // Filter candidates
60
+ let candidates = scoredFacts;
61
+ if (!allowCriticalEviction) {
62
+ // Exclude critical facts
63
+ candidates = scoredFacts.filter((sf) => !this.isCritical(sf.fact));
64
+ }
65
+ // Calculate number of facts to evict
66
+ // Assume average fact contributes equally to fill
67
+ const avgFillPerFact = session.injected_facts.length > 0
68
+ ? currentFill / session.injected_facts.length
69
+ : 0.01; // Default small contribution
70
+ let factsToEvictCount = Math.max(EVICTION_CONFIG.MIN_EVICTION_COUNT, Math.ceil(fillToReduce / avgFillPerFact));
71
+ // Don't evict more than available candidates
72
+ factsToEvictCount = Math.min(factsToEvictCount, candidates.length);
73
+ // Select facts to evict
74
+ const factsToEvict = candidates
75
+ .slice(0, factsToEvictCount)
76
+ .map((sf) => ({
77
+ fact: sf.fact,
78
+ score: sf.score,
79
+ }));
80
+ // Calculate expected fill after eviction
81
+ const expectedFillAfter = Math.max(0, currentFill - factsToEvictCount * avgFillPerFact);
82
+ // Check if plan includes critical facts
83
+ const includesCritical = factsToEvict.some((f) => this.isCritical(f.fact));
84
+ return {
85
+ factsToEvict,
86
+ expectedFillAfter,
87
+ includesCritical,
88
+ };
89
+ }
90
+ /**
91
+ * Execute eviction on a session
92
+ * Removes lowest-scored facts to reduce estimated fill
93
+ *
94
+ * @param session - Current session state
95
+ * @returns Eviction result with details of what was evicted
96
+ */
97
+ async runEviction(session) {
98
+ const plan = this.getEvictionPlan(session);
99
+ // Extract facts to evict
100
+ const evictedFacts = plan.factsToEvict.map((f) => f.fact);
101
+ // Log eviction operation
102
+ mcpLogger.info('MEMORY', `Evicting ${evictedFacts.length} facts from session ${session.session_id} (fill: ${(session.estimated_fill * 100).toFixed(1)}% → ${(plan.expectedFillAfter * 100).toFixed(1)}%)`);
103
+ // Log if critical facts are being evicted
104
+ if (plan.includesCritical) {
105
+ mcpLogger.warn('MEMORY', `Eviction includes critical facts (fill > ${EVICTION_CONFIG.CRITICAL_EVICTION_THRESHOLD * 100}%)`);
106
+ }
107
+ // Debug: log each evicted fact
108
+ for (const { fact, score } of plan.factsToEvict) {
109
+ mcpLogger.debug('MEMORY', ` Evicting: ${fact.edge_id} (score: ${score.toFixed(3)}, critical: ${this.isCritical(fact)})`);
110
+ }
111
+ return {
112
+ evictedCount: evictedFacts.length,
113
+ evictedFacts,
114
+ newEstimatedFill: plan.expectedFillAfter,
115
+ summaryGenerated: undefined, // Future: could generate a summary of evicted context
116
+ };
117
+ }
118
+ /**
119
+ * Get eviction statistics for a session
120
+ * Useful for monitoring and debugging
121
+ *
122
+ * @param session - Current session state
123
+ * @returns Statistics about eviction potential
124
+ */
125
+ getEvictionStats(session) {
126
+ const scoredFacts = this.scorer.scoreAllFacts(session);
127
+ const criticalCount = scoredFacts.filter((sf) => this.isCritical(sf.fact)).length;
128
+ const scores = scoredFacts.map((sf) => sf.score);
129
+ const avgScore = scores.length > 0
130
+ ? scores.reduce((sum, s) => sum + s, 0) / scores.length
131
+ : 0;
132
+ return {
133
+ currentFill: session.estimated_fill,
134
+ shouldEvict: this.shouldEvict(session),
135
+ criticalFactCount: criticalCount,
136
+ normalFactCount: session.injected_facts.length - criticalCount,
137
+ averageScore: avgScore,
138
+ lowestScore: scores.length > 0 ? Math.min(...scores) : 0,
139
+ highestScore: scores.length > 0 ? Math.max(...scores) : 0,
140
+ };
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Private Helpers
144
+ // ---------------------------------------------------------------------------
145
+ /**
146
+ * Check if a fact is marked as critical
147
+ */
148
+ isCritical(fact) {
149
+ const metadata = fact.metadata;
150
+ if (!metadata)
151
+ return false;
152
+ // Check explicit critical flag
153
+ if (metadata.critical)
154
+ return true;
155
+ // Check category-based critical status
156
+ if (metadata.category === 'architecture')
157
+ return true;
158
+ return false;
159
+ }
160
+ }
161
+ // -----------------------------------------------------------------------------
162
+ // Factory
163
+ // -----------------------------------------------------------------------------
164
+ /**
165
+ * Create a new eviction engine instance
166
+ *
167
+ * @param scorer - Optional importance scorer (creates new one if not provided)
168
+ * @returns Eviction engine instance
169
+ */
170
+ export function createEvictionEngine(scorer) {
171
+ return new EvictionEngine(scorer);
172
+ }
173
+ // -----------------------------------------------------------------------------
174
+ // Exports
175
+ // -----------------------------------------------------------------------------
176
+ // Types are exported inline above via 'export interface'
@@ -0,0 +1 @@
1
+ export {};