@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,187 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// ClaudeTools Memory - Importance Scorer
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Dynamic importance scoring for automatic context management with decay
|
|
5
|
+
//
|
|
6
|
+
// Algorithm:
|
|
7
|
+
// - Base importance (critical: 1.0, high: 0.7, normal: 0.4)
|
|
8
|
+
// - Exponential time decay (half-life: 30 minutes)
|
|
9
|
+
// - Reference boost (recently-referenced facts stay longer)
|
|
10
|
+
// - Recency boost (newer injections weighted higher)
|
|
11
|
+
//
|
|
12
|
+
// Score range: 0.0 to 1.0
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// -----------------------------------------------------------------------------
|
|
15
|
+
// Configuration
|
|
16
|
+
// -----------------------------------------------------------------------------
|
|
17
|
+
const SCORING_CONFIG = {
|
|
18
|
+
// Base importance weights
|
|
19
|
+
BASE_IMPORTANCE: {
|
|
20
|
+
critical: 1.0,
|
|
21
|
+
high: 0.7,
|
|
22
|
+
normal: 0.4,
|
|
23
|
+
},
|
|
24
|
+
// Time decay settings
|
|
25
|
+
TIME_DECAY_HALF_LIFE_MINUTES: 30,
|
|
26
|
+
// Reference boost settings
|
|
27
|
+
REFERENCE_BOOST_DECAY_MINUTES: 10, // 10 minutes = 600,000ms
|
|
28
|
+
REFERENCE_BOOST_MIN: 0.3,
|
|
29
|
+
// Component weights (must sum to 1.0)
|
|
30
|
+
WEIGHTS: {
|
|
31
|
+
baseWithDecay: 0.5,
|
|
32
|
+
referenceBoost: 0.3,
|
|
33
|
+
recencyBoost: 0.2,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
// Importance Scorer
|
|
38
|
+
// -----------------------------------------------------------------------------
|
|
39
|
+
export class ImportanceScorer {
|
|
40
|
+
/**
|
|
41
|
+
* Calculate dynamic importance score for a single fact
|
|
42
|
+
*
|
|
43
|
+
* @param fact - The injected fact to score
|
|
44
|
+
* @param session - Current session state
|
|
45
|
+
* @returns Importance score between 0.0 and 1.0
|
|
46
|
+
*/
|
|
47
|
+
calculateImportance(fact, session) {
|
|
48
|
+
const scored = this.calculateWithBreakdown(fact, session);
|
|
49
|
+
return scored.score;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Calculate importance with component breakdown for debugging
|
|
53
|
+
*
|
|
54
|
+
* @param fact - The injected fact to score
|
|
55
|
+
* @param session - Current session state
|
|
56
|
+
* @returns Scored fact with breakdown
|
|
57
|
+
*/
|
|
58
|
+
calculateWithBreakdown(fact, session) {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
// 1. Base importance from metadata
|
|
61
|
+
const importance = this.extractImportanceLevel(fact);
|
|
62
|
+
const baseImportance = SCORING_CONFIG.BASE_IMPORTANCE[importance];
|
|
63
|
+
// 2. Time decay (exponential with half-life)
|
|
64
|
+
const minutesSinceInjection = (now - fact.injected_at.getTime()) / 60000;
|
|
65
|
+
const timeDecay = Math.exp((-0.693 * minutesSinceInjection) /
|
|
66
|
+
SCORING_CONFIG.TIME_DECAY_HALF_LIFE_MINUTES);
|
|
67
|
+
// 3. Reference boost (facts that were recently referenced stay longer)
|
|
68
|
+
const referenceBoost = this.calculateReferenceBoost(fact, now);
|
|
69
|
+
// 4. Recency of injection (newer facts weighted higher in session)
|
|
70
|
+
const recencyBoost = this.calculateRecencyBoost(fact, session, now);
|
|
71
|
+
// Combine components with weights
|
|
72
|
+
const score = baseImportance * timeDecay * SCORING_CONFIG.WEIGHTS.baseWithDecay +
|
|
73
|
+
referenceBoost * SCORING_CONFIG.WEIGHTS.referenceBoost +
|
|
74
|
+
recencyBoost * SCORING_CONFIG.WEIGHTS.recencyBoost;
|
|
75
|
+
return {
|
|
76
|
+
fact,
|
|
77
|
+
score: Math.max(0, Math.min(1, score)), // Clamp to [0, 1]
|
|
78
|
+
breakdown: {
|
|
79
|
+
base: baseImportance,
|
|
80
|
+
timeDecay,
|
|
81
|
+
referenceBoost,
|
|
82
|
+
recencyBoost,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Score all facts in a session
|
|
88
|
+
*
|
|
89
|
+
* @param session - Session state with injected facts
|
|
90
|
+
* @returns Array of scored facts sorted by score (descending)
|
|
91
|
+
*/
|
|
92
|
+
scoreAllFacts(session) {
|
|
93
|
+
return session.injected_facts
|
|
94
|
+
.map((fact) => this.calculateWithBreakdown(fact, session))
|
|
95
|
+
.sort((a, b) => b.score - a.score);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get facts that should be evicted (lowest importance scores)
|
|
99
|
+
*
|
|
100
|
+
* @param session - Session state
|
|
101
|
+
* @param count - Number of facts to evict
|
|
102
|
+
* @returns Facts to evict (lowest scores first)
|
|
103
|
+
*/
|
|
104
|
+
getEvictionCandidates(session, count) {
|
|
105
|
+
const scored = this.scoreAllFacts(session);
|
|
106
|
+
return scored
|
|
107
|
+
.slice(-count) // Take last N (lowest scores)
|
|
108
|
+
.map((s) => s.fact);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get facts above a minimum importance threshold
|
|
112
|
+
*
|
|
113
|
+
* @param session - Session state
|
|
114
|
+
* @param minScore - Minimum score threshold (0.0 to 1.0)
|
|
115
|
+
* @returns Facts above threshold
|
|
116
|
+
*/
|
|
117
|
+
getFactsAboveThreshold(session, minScore) {
|
|
118
|
+
const scored = this.scoreAllFacts(session);
|
|
119
|
+
return scored.filter((s) => s.score >= minScore).map((s) => s.fact);
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Private Helpers
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Extract importance level from fact metadata
|
|
126
|
+
*/
|
|
127
|
+
extractImportanceLevel(fact) {
|
|
128
|
+
const metadata = fact.metadata;
|
|
129
|
+
if (!metadata) {
|
|
130
|
+
return 'normal';
|
|
131
|
+
}
|
|
132
|
+
// Check explicit importance flags
|
|
133
|
+
if (metadata.critical)
|
|
134
|
+
return 'critical';
|
|
135
|
+
if (metadata.important)
|
|
136
|
+
return 'high';
|
|
137
|
+
// Check category-based importance
|
|
138
|
+
if (metadata.category === 'architecture')
|
|
139
|
+
return 'critical';
|
|
140
|
+
if (metadata.category === 'pattern')
|
|
141
|
+
return 'high';
|
|
142
|
+
if (metadata.category === 'decision')
|
|
143
|
+
return 'high';
|
|
144
|
+
return 'normal';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Calculate reference boost
|
|
148
|
+
* Recently-referenced facts get a boost to stay in context longer
|
|
149
|
+
*/
|
|
150
|
+
calculateReferenceBoost(fact, now) {
|
|
151
|
+
if (!fact.last_referenced) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
const minutesSinceReference = (now - fact.last_referenced.getTime()) / 60000;
|
|
155
|
+
const decayMs = SCORING_CONFIG.REFERENCE_BOOST_DECAY_MINUTES * 60000;
|
|
156
|
+
const timeSinceReference = now - fact.last_referenced.getTime();
|
|
157
|
+
// Linear decay from 1.0 to min over decay period
|
|
158
|
+
const boost = Math.max(SCORING_CONFIG.REFERENCE_BOOST_MIN, 1 - timeSinceReference / decayMs);
|
|
159
|
+
return boost;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Calculate recency boost
|
|
163
|
+
* Newer injections in the session get a boost
|
|
164
|
+
*/
|
|
165
|
+
calculateRecencyBoost(fact, session, now) {
|
|
166
|
+
const sessionDuration = now - session.started_at.getTime();
|
|
167
|
+
if (sessionDuration <= 0) {
|
|
168
|
+
return 1; // Session just started, all facts are equally recent
|
|
169
|
+
}
|
|
170
|
+
const factAge = now - fact.injected_at.getTime();
|
|
171
|
+
const recency = 1 - factAge / sessionDuration;
|
|
172
|
+
return Math.max(0, Math.min(1, recency)); // Clamp to [0, 1]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// -----------------------------------------------------------------------------
|
|
176
|
+
// Factory
|
|
177
|
+
// -----------------------------------------------------------------------------
|
|
178
|
+
/**
|
|
179
|
+
* Create a new importance scorer instance
|
|
180
|
+
*/
|
|
181
|
+
export function createImportanceScorer() {
|
|
182
|
+
return new ImportanceScorer();
|
|
183
|
+
}
|
|
184
|
+
// -----------------------------------------------------------------------------
|
|
185
|
+
// Exports
|
|
186
|
+
// -----------------------------------------------------------------------------
|
|
187
|
+
// Types are exported inline above via 'export interface' and 'export type'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { UsageEstimator, usageEstimator, estimateTokens, isContextNearLimit, getContextFill, MODEL_CONTEXT_LIMITS, DEFAULT_MODEL_LIMIT, type TokenUsage, type UsageSnapshot, } from './usage-estimator.js';
|
|
2
|
+
export { ImportanceScorer, createImportanceScorer, type InjectedFact, type SessionState, type ScoredImportance, type ImportanceLevel, } from './importance-scorer.js';
|
|
3
|
+
export { SessionStore, getSessionStore, closeSessionStore, CONTEXT_LIMITS, type ModelType, type Exchange, type PersistedSessionState, } from './session-store.js';
|
|
4
|
+
export { type DeduplicationTracker, InMemoryDeduplicationTracker, createDeduplicationTracker, } from './deduplication.js';
|
|
5
|
+
export { EvictionEngine, createEvictionEngine, type EvictionResult, type EvictionPlan, } from './eviction-engine.js';
|
|
6
|
+
export { HealthMonitor, createHealthMonitor, getHealthMonitor, resetHealthMonitor, type HealthStatus, type DriftRecord, } from './health-monitor.js';
|
|
7
|
+
export { ExchangeSummariser, createExchangeSummariser, type SessionStateWithExchanges, type SummarisationResult, type AIEnvironment, } from './exchange-summariser.js';
|
|
8
|
+
export { EmergencyEviction, createEmergencyEviction, type EmergencyResult, type EmergencyConfig, } from './emergency-eviction.js';
|
|
9
|
+
export { getSessionStateJSON, } from './session-helper.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Context Module Exports
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Automatic context management, token usage tracking, and injection control.
|
|
6
|
+
//
|
|
7
|
+
// =============================================================================
|
|
8
|
+
export { UsageEstimator, usageEstimator, estimateTokens, isContextNearLimit, getContextFill, MODEL_CONTEXT_LIMITS, DEFAULT_MODEL_LIMIT, } from './usage-estimator.js';
|
|
9
|
+
export { ImportanceScorer, createImportanceScorer, } from './importance-scorer.js';
|
|
10
|
+
export { SessionStore, getSessionStore, closeSessionStore, CONTEXT_LIMITS, } from './session-store.js';
|
|
11
|
+
export { InMemoryDeduplicationTracker, createDeduplicationTracker, } from './deduplication.js';
|
|
12
|
+
export { EvictionEngine, createEvictionEngine, } from './eviction-engine.js';
|
|
13
|
+
export { HealthMonitor, createHealthMonitor, getHealthMonitor, resetHealthMonitor, } from './health-monitor.js';
|
|
14
|
+
export { ExchangeSummariser, createExchangeSummariser, } from './exchange-summariser.js';
|
|
15
|
+
export { EmergencyEviction, createEmergencyEviction, } from './emergency-eviction.js';
|
|
16
|
+
export { getSessionStateJSON, } from './session-helper.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current session state for injection budget calculation
|
|
3
|
+
*
|
|
4
|
+
* Returns JSON with estimated_fill and context_limit for the current session.
|
|
5
|
+
* If no session found or error occurs, returns empty object for graceful degradation.
|
|
6
|
+
*
|
|
7
|
+
* Usage from bash:
|
|
8
|
+
* SESSION_STATE=$(node -e "import('./session-helper.js').then(m => m.getSessionStateJSON('$SESSION_ID'))")
|
|
9
|
+
*/
|
|
10
|
+
export declare function getSessionStateJSON(sessionId: string): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Session Helper - Bridge between hooks and SessionStore
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Provides a simple CLI interface for bash hooks to query session state
|
|
5
|
+
// =============================================================================
|
|
6
|
+
import { getSessionStore } from './session-store.js';
|
|
7
|
+
import { mcpLogger } from '../logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Get current session state for injection budget calculation
|
|
10
|
+
*
|
|
11
|
+
* Returns JSON with estimated_fill and context_limit for the current session.
|
|
12
|
+
* If no session found or error occurs, returns empty object for graceful degradation.
|
|
13
|
+
*
|
|
14
|
+
* Usage from bash:
|
|
15
|
+
* SESSION_STATE=$(node -e "import('./session-helper.js').then(m => m.getSessionStateJSON('$SESSION_ID'))")
|
|
16
|
+
*/
|
|
17
|
+
export async function getSessionStateJSON(sessionId) {
|
|
18
|
+
try {
|
|
19
|
+
const store = getSessionStore();
|
|
20
|
+
const session = await store.getSession(sessionId);
|
|
21
|
+
if (!session) {
|
|
22
|
+
// No session found - return empty object for graceful degradation
|
|
23
|
+
console.log('{}');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Return minimal state needed for budget calculation
|
|
27
|
+
const state = {
|
|
28
|
+
estimated_fill: session.estimated_fill,
|
|
29
|
+
context_limit: session.context_limit,
|
|
30
|
+
};
|
|
31
|
+
console.log(JSON.stringify(state));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// Error occurred - log for debugging but return empty for graceful degradation
|
|
35
|
+
mcpLogger.error('SESSION_HELPER', `Failed to get session state: ${error}`);
|
|
36
|
+
console.log('{}');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// CLI entry point
|
|
40
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
41
|
+
const sessionId = process.argv[2];
|
|
42
|
+
if (!sessionId) {
|
|
43
|
+
console.error('Usage: session-helper.ts <session_id>');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
getSessionStateJSON(sessionId).catch((error) => {
|
|
47
|
+
mcpLogger.error('SESSION_HELPER', `Error: ${error}`);
|
|
48
|
+
console.log('{}');
|
|
49
|
+
process.exit(0); // Exit 0 for graceful degradation
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { InjectedFact } from './importance-scorer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Model types supported by Claude Code
|
|
4
|
+
*/
|
|
5
|
+
export type ModelType = 'sonnet' | 'opus' | 'haiku';
|
|
6
|
+
/**
|
|
7
|
+
* Context window limits by model (in tokens)
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONTEXT_LIMITS: Record<ModelType, number>;
|
|
10
|
+
/**
|
|
11
|
+
* An exchange (turn) in the conversation
|
|
12
|
+
*/
|
|
13
|
+
export interface Exchange {
|
|
14
|
+
turn: number;
|
|
15
|
+
user_tokens: number;
|
|
16
|
+
assistant_tokens: number;
|
|
17
|
+
tool_tokens: number;
|
|
18
|
+
summarised_at: Date | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Complete session state for persistence
|
|
22
|
+
* Extends the in-memory SessionState with additional persistence fields
|
|
23
|
+
*/
|
|
24
|
+
export interface PersistedSessionState {
|
|
25
|
+
session_id: string;
|
|
26
|
+
started_at: Date;
|
|
27
|
+
model: ModelType;
|
|
28
|
+
context_limit: number;
|
|
29
|
+
injected_facts: InjectedFact[];
|
|
30
|
+
exchanges: Exchange[];
|
|
31
|
+
estimated_fill: number;
|
|
32
|
+
last_updated: Date;
|
|
33
|
+
}
|
|
34
|
+
export declare class SessionStore {
|
|
35
|
+
private db;
|
|
36
|
+
private dbPath;
|
|
37
|
+
constructor(dbPath?: string);
|
|
38
|
+
/**
|
|
39
|
+
* Create tables if they don't exist
|
|
40
|
+
*/
|
|
41
|
+
private initializeSchema;
|
|
42
|
+
/**
|
|
43
|
+
* Create a new session
|
|
44
|
+
*/
|
|
45
|
+
createSession(sessionId: string, model: ModelType): Promise<PersistedSessionState>;
|
|
46
|
+
/**
|
|
47
|
+
* Get session state by ID
|
|
48
|
+
*/
|
|
49
|
+
getSession(sessionId: string): Promise<PersistedSessionState | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Update session state
|
|
52
|
+
*/
|
|
53
|
+
updateSession(sessionId: string, updates: Partial<Pick<PersistedSessionState, 'estimated_fill'>>): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Add an injected fact to the session
|
|
56
|
+
*/
|
|
57
|
+
addInjectedFact(sessionId: string, fact: InjectedFact): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Add an exchange to the session
|
|
60
|
+
*/
|
|
61
|
+
addExchange(sessionId: string, exchange: Exchange): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Update last_referenced for a fact
|
|
64
|
+
*/
|
|
65
|
+
updateFactReference(sessionId: string, edgeId: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Mark an exchange as summarised
|
|
68
|
+
*/
|
|
69
|
+
markExchangeSummarised(sessionId: string, turn: number): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Delete a session and all its data
|
|
72
|
+
*/
|
|
73
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* List all active sessions
|
|
76
|
+
*/
|
|
77
|
+
listSessions(): Promise<Array<Pick<PersistedSessionState, 'session_id' | 'started_at' | 'model' | 'last_updated'>>>;
|
|
78
|
+
/**
|
|
79
|
+
* Clean up old sessions (older than N days)
|
|
80
|
+
*/
|
|
81
|
+
cleanupOldSessions(daysOld?: number): Promise<number>;
|
|
82
|
+
/**
|
|
83
|
+
* Close the database connection
|
|
84
|
+
*/
|
|
85
|
+
close(): void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get or create the singleton session store instance
|
|
89
|
+
*/
|
|
90
|
+
export declare function getSessionStore(): SessionStore;
|
|
91
|
+
/**
|
|
92
|
+
* Close the singleton instance (for cleanup)
|
|
93
|
+
*/
|
|
94
|
+
export declare function closeSessionStore(): void;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Session State Store - Local SQLite storage for context management
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Stores session state including injected facts, exchanges, and token estimates
|
|
5
|
+
// for automatic context window management in Claude Code sessions.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { mcpLogger } from '../logger.js';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import { getConfigDir } from '../helpers/config-manager.js';
|
|
12
|
+
/**
|
|
13
|
+
* Context window limits by model (in tokens)
|
|
14
|
+
*/
|
|
15
|
+
export const CONTEXT_LIMITS = {
|
|
16
|
+
sonnet: 200_000,
|
|
17
|
+
opus: 200_000,
|
|
18
|
+
haiku: 200_000,
|
|
19
|
+
};
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// Session Store Implementation
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
export class SessionStore {
|
|
24
|
+
db;
|
|
25
|
+
dbPath;
|
|
26
|
+
constructor(dbPath) {
|
|
27
|
+
// Default to ~/.claudetools/sessions.db
|
|
28
|
+
this.dbPath = dbPath || path.join(getConfigDir(), 'sessions.db');
|
|
29
|
+
// Ensure directory exists
|
|
30
|
+
const dir = path.dirname(this.dbPath);
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
// Open database
|
|
35
|
+
this.db = new Database(this.dbPath);
|
|
36
|
+
this.db.pragma('journal_mode = WAL'); // Better concurrency
|
|
37
|
+
this.db.pragma('foreign_keys = ON');
|
|
38
|
+
// Initialize schema
|
|
39
|
+
this.initializeSchema();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create tables if they don't exist
|
|
43
|
+
*/
|
|
44
|
+
initializeSchema() {
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
47
|
+
session_id TEXT PRIMARY KEY,
|
|
48
|
+
started_at TEXT NOT NULL,
|
|
49
|
+
model TEXT NOT NULL CHECK(model IN ('sonnet', 'opus', 'haiku')),
|
|
50
|
+
context_limit INTEGER NOT NULL,
|
|
51
|
+
estimated_fill REAL NOT NULL DEFAULT 0,
|
|
52
|
+
last_updated TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS injected_facts (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
fact_data TEXT NOT NULL,
|
|
59
|
+
injected_at TEXT NOT NULL,
|
|
60
|
+
last_referenced TEXT,
|
|
61
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS exchanges (
|
|
65
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
|
+
session_id TEXT NOT NULL,
|
|
67
|
+
turn INTEGER NOT NULL,
|
|
68
|
+
user_tokens INTEGER NOT NULL,
|
|
69
|
+
assistant_tokens INTEGER NOT NULL,
|
|
70
|
+
tool_tokens INTEGER NOT NULL,
|
|
71
|
+
summarised_at TEXT,
|
|
72
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
-- Indexes for performance
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_injected_facts_session
|
|
77
|
+
ON injected_facts(session_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_exchanges_session
|
|
79
|
+
ON exchanges(session_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_exchanges_turn
|
|
81
|
+
ON exchanges(session_id, turn);
|
|
82
|
+
`);
|
|
83
|
+
mcpLogger.debug('MEMORY', `Initialized session database at ${this.dbPath}`);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a new session
|
|
87
|
+
*/
|
|
88
|
+
async createSession(sessionId, model) {
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
const contextLimit = CONTEXT_LIMITS[model];
|
|
91
|
+
const stmt = this.db.prepare(`
|
|
92
|
+
INSERT INTO sessions (session_id, started_at, model, context_limit, estimated_fill, last_updated)
|
|
93
|
+
VALUES (?, ?, ?, ?, 0, ?)
|
|
94
|
+
`);
|
|
95
|
+
stmt.run(sessionId, now, model, contextLimit, now);
|
|
96
|
+
mcpLogger.debug('MEMORY', `Created session ${sessionId} for model ${model}`);
|
|
97
|
+
return {
|
|
98
|
+
session_id: sessionId,
|
|
99
|
+
started_at: new Date(now),
|
|
100
|
+
model,
|
|
101
|
+
context_limit: contextLimit,
|
|
102
|
+
injected_facts: [],
|
|
103
|
+
exchanges: [],
|
|
104
|
+
estimated_fill: 0,
|
|
105
|
+
last_updated: new Date(now),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get session state by ID
|
|
110
|
+
*/
|
|
111
|
+
async getSession(sessionId) {
|
|
112
|
+
const session = this.db
|
|
113
|
+
.prepare('SELECT * FROM sessions WHERE session_id = ?')
|
|
114
|
+
.get(sessionId);
|
|
115
|
+
if (!session) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Load injected facts
|
|
119
|
+
const factRows = this.db
|
|
120
|
+
.prepare('SELECT * FROM injected_facts WHERE session_id = ? ORDER BY injected_at')
|
|
121
|
+
.all(sessionId);
|
|
122
|
+
// Load exchanges
|
|
123
|
+
const exchanges = this.db
|
|
124
|
+
.prepare('SELECT * FROM exchanges WHERE session_id = ? ORDER BY turn')
|
|
125
|
+
.all(sessionId);
|
|
126
|
+
// Parse fact data from JSON
|
|
127
|
+
const facts = factRows.map((row) => {
|
|
128
|
+
const fact = JSON.parse(row.fact_data);
|
|
129
|
+
return {
|
|
130
|
+
...fact,
|
|
131
|
+
injected_at: new Date(row.injected_at),
|
|
132
|
+
last_referenced: row.last_referenced ? new Date(row.last_referenced) : undefined,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
session_id: session.session_id,
|
|
137
|
+
started_at: new Date(session.started_at),
|
|
138
|
+
model: session.model,
|
|
139
|
+
context_limit: session.context_limit,
|
|
140
|
+
injected_facts: facts,
|
|
141
|
+
exchanges: exchanges.map((e) => ({
|
|
142
|
+
turn: e.turn,
|
|
143
|
+
user_tokens: e.user_tokens,
|
|
144
|
+
assistant_tokens: e.assistant_tokens,
|
|
145
|
+
tool_tokens: e.tool_tokens,
|
|
146
|
+
summarised_at: e.summarised_at ? new Date(e.summarised_at) : null,
|
|
147
|
+
})),
|
|
148
|
+
estimated_fill: session.estimated_fill,
|
|
149
|
+
last_updated: new Date(session.last_updated),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Update session state
|
|
154
|
+
*/
|
|
155
|
+
async updateSession(sessionId, updates) {
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
const fields = ['last_updated = ?'];
|
|
158
|
+
const values = [now];
|
|
159
|
+
if (updates.estimated_fill !== undefined) {
|
|
160
|
+
fields.push('estimated_fill = ?');
|
|
161
|
+
values.push(updates.estimated_fill);
|
|
162
|
+
}
|
|
163
|
+
values.push(sessionId);
|
|
164
|
+
const stmt = this.db.prepare(`
|
|
165
|
+
UPDATE sessions
|
|
166
|
+
SET ${fields.join(', ')}
|
|
167
|
+
WHERE session_id = ?
|
|
168
|
+
`);
|
|
169
|
+
stmt.run(...values);
|
|
170
|
+
mcpLogger.debug('MEMORY', `Updated session ${sessionId}`);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Add an injected fact to the session
|
|
174
|
+
*/
|
|
175
|
+
async addInjectedFact(sessionId, fact) {
|
|
176
|
+
// Store the fact as JSON, excluding the temporal fields we track separately
|
|
177
|
+
const { injected_at, last_referenced, ...factData } = fact;
|
|
178
|
+
const stmt = this.db.prepare(`
|
|
179
|
+
INSERT INTO injected_facts (session_id, fact_data, injected_at, last_referenced)
|
|
180
|
+
VALUES (?, ?, ?, ?)
|
|
181
|
+
`);
|
|
182
|
+
stmt.run(sessionId, JSON.stringify(factData), injected_at.toISOString(), last_referenced ? last_referenced.toISOString() : null);
|
|
183
|
+
mcpLogger.debug('MEMORY', `Added fact ${fact.edge_id} to session ${sessionId}`);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Add an exchange to the session
|
|
187
|
+
*/
|
|
188
|
+
async addExchange(sessionId, exchange) {
|
|
189
|
+
const stmt = this.db.prepare(`
|
|
190
|
+
INSERT INTO exchanges (session_id, turn, user_tokens, assistant_tokens, tool_tokens, summarised_at)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
192
|
+
`);
|
|
193
|
+
stmt.run(sessionId, exchange.turn, exchange.user_tokens, exchange.assistant_tokens, exchange.tool_tokens, exchange.summarised_at ? exchange.summarised_at.toISOString() : null);
|
|
194
|
+
mcpLogger.debug('MEMORY', `Added exchange turn ${exchange.turn} to session ${sessionId}`);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Update last_referenced for a fact
|
|
198
|
+
*/
|
|
199
|
+
async updateFactReference(sessionId, edgeId) {
|
|
200
|
+
const now = new Date().toISOString();
|
|
201
|
+
// Need to find the fact by parsing JSON - SQLite JSON functions
|
|
202
|
+
const stmt = this.db.prepare(`
|
|
203
|
+
UPDATE injected_facts
|
|
204
|
+
SET last_referenced = ?
|
|
205
|
+
WHERE session_id = ? AND json_extract(fact_data, '$.edge_id') = ?
|
|
206
|
+
`);
|
|
207
|
+
stmt.run(now, sessionId, edgeId);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Mark an exchange as summarised
|
|
211
|
+
*/
|
|
212
|
+
async markExchangeSummarised(sessionId, turn) {
|
|
213
|
+
const now = new Date().toISOString();
|
|
214
|
+
const stmt = this.db.prepare(`
|
|
215
|
+
UPDATE exchanges
|
|
216
|
+
SET summarised_at = ?
|
|
217
|
+
WHERE session_id = ? AND turn = ?
|
|
218
|
+
`);
|
|
219
|
+
stmt.run(now, sessionId, turn);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Delete a session and all its data
|
|
223
|
+
*/
|
|
224
|
+
async deleteSession(sessionId) {
|
|
225
|
+
const stmt = this.db.prepare('DELETE FROM sessions WHERE session_id = ?');
|
|
226
|
+
stmt.run(sessionId);
|
|
227
|
+
mcpLogger.debug('MEMORY', `Deleted session ${sessionId}`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* List all active sessions
|
|
231
|
+
*/
|
|
232
|
+
async listSessions() {
|
|
233
|
+
const sessions = this.db
|
|
234
|
+
.prepare('SELECT session_id, started_at, model, last_updated FROM sessions ORDER BY started_at DESC')
|
|
235
|
+
.all();
|
|
236
|
+
return sessions.map((s) => ({
|
|
237
|
+
session_id: s.session_id,
|
|
238
|
+
started_at: new Date(s.started_at),
|
|
239
|
+
model: s.model,
|
|
240
|
+
last_updated: new Date(s.last_updated),
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Clean up old sessions (older than N days)
|
|
245
|
+
*/
|
|
246
|
+
async cleanupOldSessions(daysOld = 7) {
|
|
247
|
+
const cutoffDate = new Date();
|
|
248
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
249
|
+
const stmt = this.db.prepare(`
|
|
250
|
+
DELETE FROM sessions
|
|
251
|
+
WHERE started_at < ?
|
|
252
|
+
`);
|
|
253
|
+
const result = stmt.run(cutoffDate.toISOString());
|
|
254
|
+
mcpLogger.info('MEMORY', `Cleaned up ${result.changes} sessions older than ${daysOld} days`);
|
|
255
|
+
return result.changes;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Close the database connection
|
|
259
|
+
*/
|
|
260
|
+
close() {
|
|
261
|
+
this.db.close();
|
|
262
|
+
mcpLogger.debug('MEMORY', 'Closed session database connection');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// -----------------------------------------------------------------------------
|
|
266
|
+
// Singleton Instance
|
|
267
|
+
// -----------------------------------------------------------------------------
|
|
268
|
+
let storeInstance = null;
|
|
269
|
+
/**
|
|
270
|
+
* Get or create the singleton session store instance
|
|
271
|
+
*/
|
|
272
|
+
export function getSessionStore() {
|
|
273
|
+
if (!storeInstance) {
|
|
274
|
+
storeInstance = new SessionStore();
|
|
275
|
+
}
|
|
276
|
+
return storeInstance;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Close the singleton instance (for cleanup)
|
|
280
|
+
*/
|
|
281
|
+
export function closeSessionStore() {
|
|
282
|
+
if (storeInstance) {
|
|
283
|
+
storeInstance.close();
|
|
284
|
+
storeInstance = null;
|
|
285
|
+
}
|
|
286
|
+
}
|