@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.
- package/dist/cli.js +41 -0
- package/dist/context/deduplication.d.ts +72 -0
- package/dist/context/deduplication.js +77 -0
- package/dist/context/deduplication.test.d.ts +6 -0
- package/dist/context/deduplication.test.js +84 -0
- package/dist/context/emergency-eviction.d.ts +73 -0
- package/dist/context/emergency-eviction.example.d.ts +13 -0
- package/dist/context/emergency-eviction.example.js +94 -0
- package/dist/context/emergency-eviction.js +226 -0
- package/dist/context/eviction-engine.d.ts +76 -0
- package/dist/context/eviction-engine.example.d.ts +7 -0
- package/dist/context/eviction-engine.example.js +144 -0
- package/dist/context/eviction-engine.js +176 -0
- package/dist/context/example-usage.d.ts +1 -0
- package/dist/context/example-usage.js +128 -0
- package/dist/context/exchange-summariser.d.ts +80 -0
- package/dist/context/exchange-summariser.js +261 -0
- package/dist/context/health-monitor.d.ts +97 -0
- package/dist/context/health-monitor.example.d.ts +1 -0
- package/dist/context/health-monitor.example.js +164 -0
- package/dist/context/health-monitor.js +210 -0
- package/dist/context/importance-scorer.d.ts +94 -0
- package/dist/context/importance-scorer.example.d.ts +1 -0
- package/dist/context/importance-scorer.example.js +140 -0
- package/dist/context/importance-scorer.js +187 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.js +16 -0
- package/dist/context/session-helper.d.ts +10 -0
- package/dist/context/session-helper.js +51 -0
- package/dist/context/session-store.d.ts +94 -0
- package/dist/context/session-store.js +286 -0
- package/dist/context/usage-estimator.d.ts +131 -0
- package/dist/context/usage-estimator.js +260 -0
- package/dist/context/usage-estimator.test.d.ts +1 -0
- package/dist/context/usage-estimator.test.js +208 -0
- package/dist/context-cli.d.ts +16 -0
- package/dist/context-cli.js +309 -0
- package/dist/evaluation/build-dataset.d.ts +1 -0
- package/dist/evaluation/build-dataset.js +135 -0
- package/dist/evaluation/threshold-eval.d.ts +63 -0
- package/dist/evaluation/threshold-eval.js +250 -0
- package/dist/handlers/codedna-handlers.d.ts +2 -2
- package/dist/handlers/tool-handlers.js +126 -165
- package/dist/helpers/api-client.d.ts +5 -1
- package/dist/helpers/api-client.js +3 -1
- package/dist/helpers/compact-formatter.d.ts +51 -0
- package/dist/helpers/compact-formatter.js +130 -0
- package/dist/helpers/engagement-tracker.d.ts +10 -0
- package/dist/helpers/engagement-tracker.js +61 -0
- package/dist/helpers/error-tracking.js +1 -1
- package/dist/helpers/session-validation.d.ts +76 -0
- package/dist/helpers/session-validation.js +221 -0
- package/dist/helpers/usage-analytics.js +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/post-tool-use-hook-cli.d.ts +2 -0
- package/dist/hooks/post-tool-use-hook-cli.js +34 -0
- package/dist/hooks/post-tool-use.d.ts +67 -0
- package/dist/hooks/post-tool-use.js +234 -0
- package/dist/hooks/stop-hook-cli.d.ts +2 -0
- package/dist/hooks/stop-hook-cli.js +34 -0
- package/dist/hooks/stop.d.ts +64 -0
- package/dist/hooks/stop.js +192 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +4 -0
- package/dist/resources.js +3 -0
- package/dist/setup.js +206 -2
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +23 -35
- package/dist/templates/worker-prompt.js +35 -202
- package/dist/tools.js +26 -20
- 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 {};
|