claude-memory-layer 1.0.9 → 1.0.10
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/.history/package_20260202121115.json +49 -0
- package/dist/cli/index.js +107 -3
- package/dist/cli/index.js.map +2 -2
- package/dist/core/index.js +78 -0
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +107 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +107 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +107 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +107 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +140 -70
- package/dist/hooks/user-prompt-submit.js.map +2 -2
- package/dist/server/api/index.js +107 -3
- package/dist/server/api/index.js.map +2 -2
- package/dist/server/index.js +107 -3
- package/dist/server/index.js.map +2 -2
- package/dist/services/memory-service.js +124 -3
- package/dist/services/memory-service.js.map +2 -2
- package/package.json +1 -1
- package/src/core/sqlite-event-store.ts +98 -0
- package/src/hooks/user-prompt-submit.ts +34 -60
- package/src/services/memory-service.ts +71 -4
package/package.json
CHANGED
|
@@ -283,6 +283,28 @@ export class SQLiteEventStore {
|
|
|
283
283
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
284
284
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
285
285
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
286
|
+
|
|
287
|
+
-- FTS5 Full-Text Search for fast keyword search
|
|
288
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
289
|
+
content,
|
|
290
|
+
event_id UNINDEXED,
|
|
291
|
+
content='events',
|
|
292
|
+
content_rowid='rowid'
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
-- Triggers to keep FTS in sync with events table
|
|
296
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
297
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
298
|
+
END;
|
|
299
|
+
|
|
300
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
301
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
302
|
+
END;
|
|
303
|
+
|
|
304
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
305
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
306
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
307
|
+
END;
|
|
286
308
|
`);
|
|
287
309
|
|
|
288
310
|
// Migrate existing events table to add access tracking columns if they don't exist
|
|
@@ -807,6 +829,82 @@ export class SQLiteEventStore {
|
|
|
807
829
|
return rows.map(row => this.rowToEvent(row));
|
|
808
830
|
}
|
|
809
831
|
|
|
832
|
+
/**
|
|
833
|
+
* Fast keyword search using FTS5
|
|
834
|
+
* Returns events matching the search query, ranked by relevance
|
|
835
|
+
*/
|
|
836
|
+
async keywordSearch(query: string, limit: number = 10): Promise<Array<{event: MemoryEvent; rank: number}>> {
|
|
837
|
+
await this.initialize();
|
|
838
|
+
|
|
839
|
+
// Escape special FTS5 characters and prepare search terms
|
|
840
|
+
const searchTerms = query
|
|
841
|
+
.replace(/['"(){}[\]^~*?:\\/-]/g, ' ') // Remove special chars
|
|
842
|
+
.split(/\s+/)
|
|
843
|
+
.filter(term => term.length > 1) // Filter short terms
|
|
844
|
+
.map(term => `"${term}"*`) // Prefix matching
|
|
845
|
+
.join(' OR ');
|
|
846
|
+
|
|
847
|
+
if (!searchTerms) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
853
|
+
this.db,
|
|
854
|
+
`SELECT e.*, fts.rank
|
|
855
|
+
FROM events_fts fts
|
|
856
|
+
JOIN events e ON e.id = fts.event_id
|
|
857
|
+
WHERE events_fts MATCH ?
|
|
858
|
+
ORDER BY fts.rank
|
|
859
|
+
LIMIT ?`,
|
|
860
|
+
[searchTerms, limit]
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
return rows.map(row => ({
|
|
864
|
+
event: this.rowToEvent(row),
|
|
865
|
+
rank: row.rank as number
|
|
866
|
+
}));
|
|
867
|
+
} catch (error: any) {
|
|
868
|
+
// FTS table might not exist yet (old database)
|
|
869
|
+
// Fallback to LIKE search
|
|
870
|
+
const likePattern = `%${query}%`;
|
|
871
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
872
|
+
this.db,
|
|
873
|
+
`SELECT *, 0 as rank FROM events
|
|
874
|
+
WHERE content LIKE ?
|
|
875
|
+
ORDER BY timestamp DESC
|
|
876
|
+
LIMIT ?`,
|
|
877
|
+
[likePattern, limit]
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
return rows.map(row => ({
|
|
881
|
+
event: this.rowToEvent(row),
|
|
882
|
+
rank: 0
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Rebuild FTS index from existing events
|
|
889
|
+
* Call this once after upgrading to FTS5
|
|
890
|
+
*/
|
|
891
|
+
async rebuildFtsIndex(): Promise<number> {
|
|
892
|
+
await this.initialize();
|
|
893
|
+
|
|
894
|
+
// Get count of events to index
|
|
895
|
+
const countRow = sqliteGet<{count: number}>(this.db, 'SELECT COUNT(*) as count FROM events', []);
|
|
896
|
+
const totalEvents = countRow?.count ?? 0;
|
|
897
|
+
|
|
898
|
+
// Clear and rebuild FTS index
|
|
899
|
+
sqliteExec(this.db, `
|
|
900
|
+
DELETE FROM events_fts;
|
|
901
|
+
INSERT INTO events_fts(rowid, content, event_id)
|
|
902
|
+
SELECT rowid, content, id FROM events;
|
|
903
|
+
`);
|
|
904
|
+
|
|
905
|
+
return totalEvents;
|
|
906
|
+
}
|
|
907
|
+
|
|
810
908
|
/**
|
|
811
909
|
* Get database instance for direct access
|
|
812
910
|
*/
|
|
@@ -1,93 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* User Prompt Submit Hook
|
|
4
|
-
* Called when user submits a prompt - retrieves relevant memories
|
|
4
|
+
* Called when user submits a prompt - retrieves relevant memories using fast keyword search
|
|
5
|
+
*
|
|
6
|
+
* Uses SQLite FTS5 for fast keyword-based search (no ML model needed)
|
|
7
|
+
* Much faster than vector search (~100ms vs 3-5s)
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
import {
|
|
10
|
+
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
8
11
|
import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
|
|
9
12
|
|
|
13
|
+
// Configuration
|
|
14
|
+
const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
|
|
15
|
+
const MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.3');
|
|
16
|
+
const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
|
|
17
|
+
|
|
10
18
|
async function main(): Promise<void> {
|
|
11
19
|
// Read input from stdin
|
|
12
20
|
const inputData = await readStdin();
|
|
13
21
|
const input: UserPromptSubmitInput = JSON.parse(inputData);
|
|
14
22
|
|
|
15
|
-
//
|
|
16
|
-
const memoryService =
|
|
17
|
-
|
|
18
|
-
// Configuration from environment variables
|
|
19
|
-
const config = {
|
|
20
|
-
candidateCount: parseInt(process.env.CLAUDE_MEMORY_CANDIDATES || '10'),
|
|
21
|
-
minScore: parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.7'),
|
|
22
|
-
maxMemories: parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5'),
|
|
23
|
-
dynamicThresholdRatio: parseFloat(process.env.CLAUDE_MEMORY_THRESHOLD_RATIO || '0.85'),
|
|
24
|
-
strictMinScore: parseFloat(process.env.CLAUDE_MEMORY_STRICT_MIN || '0.75')
|
|
25
|
-
};
|
|
23
|
+
// Use lightweight service (SQLite only, no embedder/vector - FAST!)
|
|
24
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
26
25
|
|
|
27
26
|
try {
|
|
28
|
-
// Check if shared store is enabled
|
|
29
|
-
const includeShared = memoryService.isSharedStoreEnabled();
|
|
30
|
-
|
|
31
|
-
// Retrieve relevant memories for the prompt (including shared if enabled)
|
|
32
|
-
// First, get more candidates to filter from
|
|
33
|
-
const retrievalResult = await memoryService.retrieveMemories(input.prompt, {
|
|
34
|
-
topK: config.candidateCount, // Get more candidates initially
|
|
35
|
-
minScore: config.minScore,
|
|
36
|
-
includeShared
|
|
37
|
-
});
|
|
38
|
-
|
|
39
27
|
// Store the user prompt for future retrieval
|
|
40
28
|
await memoryService.storeUserPrompt(
|
|
41
29
|
input.session_id,
|
|
42
30
|
input.prompt
|
|
43
31
|
);
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
let relevantMemories = [];
|
|
47
|
-
if (retrievalResult.memories && retrievalResult.memories.length > 0) {
|
|
48
|
-
// Dynamic threshold based on the best score
|
|
49
|
-
const bestScore = Math.max(...retrievalResult.memories.map(m => m.score));
|
|
50
|
-
const dynamicThreshold = Math.max(config.strictMinScore, bestScore * config.dynamicThresholdRatio);
|
|
51
|
-
|
|
52
|
-
// Filter memories that meet the dynamic threshold
|
|
53
|
-
relevantMemories = retrievalResult.memories.filter(m => m.score >= dynamicThreshold);
|
|
33
|
+
let context = '';
|
|
54
34
|
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
// Fast keyword search if enabled
|
|
36
|
+
if (ENABLE_SEARCH && input.prompt.length > 10) {
|
|
37
|
+
const results = await memoryService.keywordSearch(input.prompt, {
|
|
38
|
+
topK: MAX_MEMORIES,
|
|
39
|
+
minScore: MIN_SCORE
|
|
40
|
+
});
|
|
57
41
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
relevantMemories = retrievalResult.memories
|
|
63
|
-
.filter(m => m.score >= config.minScore)
|
|
64
|
-
.slice(0, fallbackCount);
|
|
65
|
-
}
|
|
42
|
+
if (results.length > 0) {
|
|
43
|
+
// Increment access count for found memories
|
|
44
|
+
const eventIds = results.map(r => r.event.id);
|
|
45
|
+
await memoryService.incrementMemoryAccess(eventIds);
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
47
|
+
// Format context
|
|
48
|
+
const memories = results.map(r => {
|
|
49
|
+
const preview = r.event.content.length > 300
|
|
50
|
+
? r.event.content.substring(0, 300) + '...'
|
|
51
|
+
: r.event.content;
|
|
52
|
+
return `- [${r.event.eventType}] ${preview}`;
|
|
53
|
+
});
|
|
72
54
|
|
|
73
|
-
|
|
74
|
-
if (relevantMemories.length > 0) {
|
|
75
|
-
const eventIds = relevantMemories.map(m => m.event.id);
|
|
76
|
-
await memoryService.incrementMemoryAccess(eventIds);
|
|
55
|
+
context = `💡 **Related memories found:**\n\n${memories.join('\n\n')}`;
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
|
|
80
|
-
// Format context for Claude with only the relevant memories
|
|
81
|
-
const filteredResult = {
|
|
82
|
-
...retrievalResult,
|
|
83
|
-
memories: relevantMemories
|
|
84
|
-
};
|
|
85
|
-
const context = memoryService.formatAsContext(filteredResult);
|
|
86
|
-
|
|
87
59
|
const output: UserPromptSubmitOutput = { context };
|
|
88
60
|
console.log(JSON.stringify(output));
|
|
89
61
|
} catch (error) {
|
|
90
|
-
|
|
62
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
63
|
+
console.error('Memory hook error:', error);
|
|
64
|
+
}
|
|
91
65
|
console.log(JSON.stringify({ context: '' }));
|
|
92
66
|
}
|
|
93
67
|
}
|
|
@@ -52,6 +52,8 @@ export interface MemoryServiceConfig {
|
|
|
52
52
|
readOnly?: boolean;
|
|
53
53
|
/** Enable DuckDB analytics store (default: true for server, false for hooks) */
|
|
54
54
|
analyticsEnabled?: boolean;
|
|
55
|
+
/** Lightweight mode for hooks - skip heavy initialization (default: false) */
|
|
56
|
+
lightweightMode?: boolean;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
// ============================================================
|
|
@@ -200,10 +202,12 @@ export class MemoryService {
|
|
|
200
202
|
private projectHash: string | null = null;
|
|
201
203
|
|
|
202
204
|
private readonly readOnly: boolean;
|
|
205
|
+
private readonly lightweightMode: boolean;
|
|
203
206
|
|
|
204
207
|
constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
|
|
205
208
|
const storagePath = this.expandPath(config.storagePath);
|
|
206
209
|
this.readOnly = config.readOnly ?? false;
|
|
210
|
+
this.lightweightMode = config.lightweightMode ?? false;
|
|
207
211
|
|
|
208
212
|
// Ensure storage directory exists (only if not read-only)
|
|
209
213
|
if (!this.readOnly && !fs.existsSync(storagePath)) {
|
|
@@ -272,6 +276,13 @@ export class MemoryService {
|
|
|
272
276
|
// Initialize PRIMARY store: SQLite (always)
|
|
273
277
|
await this.sqliteStore.initialize();
|
|
274
278
|
|
|
279
|
+
// Lightweight mode: only SQLite, no embedder/vector/workers
|
|
280
|
+
// Used for hooks that just need to store data quickly
|
|
281
|
+
if (this.lightweightMode) {
|
|
282
|
+
this.initialized = true;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
// Initialize analytics store if available (DuckDB)
|
|
276
287
|
if (this.analyticsStore) {
|
|
277
288
|
try {
|
|
@@ -517,10 +528,8 @@ export class MemoryService {
|
|
|
517
528
|
): Promise<UnifiedRetrievalResult> {
|
|
518
529
|
await this.initialize();
|
|
519
530
|
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
await this.vectorWorker.processAll();
|
|
523
|
-
}
|
|
531
|
+
// Note: Pending embeddings are processed by the background worker
|
|
532
|
+
// Don't block retrieval - search with whatever vectors are available
|
|
524
533
|
|
|
525
534
|
// Use unified retrieval if shared search is requested
|
|
526
535
|
if (options?.includeShared && this.sharedStore) {
|
|
@@ -534,6 +543,38 @@ export class MemoryService {
|
|
|
534
543
|
return this.retriever.retrieve(query, options);
|
|
535
544
|
}
|
|
536
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Fast keyword search using SQLite FTS5
|
|
548
|
+
* Much faster than vector search - no embedding model needed
|
|
549
|
+
*/
|
|
550
|
+
async keywordSearch(
|
|
551
|
+
query: string,
|
|
552
|
+
options?: { topK?: number; minScore?: number }
|
|
553
|
+
): Promise<Array<{event: MemoryEvent; score: number}>> {
|
|
554
|
+
await this.initialize();
|
|
555
|
+
|
|
556
|
+
const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
|
|
557
|
+
|
|
558
|
+
// Normalize FTS5 rank to a score (0-1 range)
|
|
559
|
+
// FTS5 rank is negative (higher is worse), so we convert it
|
|
560
|
+
const maxRank = Math.min(...results.map(r => r.rank), -0.001);
|
|
561
|
+
const minRank = Math.max(...results.map(r => r.rank), -1000);
|
|
562
|
+
const rankRange = maxRank - minRank || 1;
|
|
563
|
+
|
|
564
|
+
return results.map(r => ({
|
|
565
|
+
event: r.event,
|
|
566
|
+
score: 1 - (r.rank - minRank) / rankRange // Normalize to 0-1
|
|
567
|
+
})).filter(r => !options?.minScore || r.score >= options.minScore);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Rebuild FTS index (call after database upgrade)
|
|
572
|
+
*/
|
|
573
|
+
async rebuildFtsIndex(): Promise<number> {
|
|
574
|
+
await this.initialize();
|
|
575
|
+
return this.sqliteStore.rebuildFtsIndex();
|
|
576
|
+
}
|
|
577
|
+
|
|
537
578
|
/**
|
|
538
579
|
* Get session history
|
|
539
580
|
*/
|
|
@@ -1129,6 +1170,32 @@ export function getMemoryServiceForSession(sessionId: string): MemoryService {
|
|
|
1129
1170
|
return getDefaultMemoryService();
|
|
1130
1171
|
}
|
|
1131
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* Get a lightweight memory service for hooks
|
|
1175
|
+
* Only initializes SQLite - no embedder, no vector store, no workers
|
|
1176
|
+
* This is FAST (<100ms) compared to full initialization (3-5s)
|
|
1177
|
+
*/
|
|
1178
|
+
export function getLightweightMemoryService(sessionId: string): MemoryService {
|
|
1179
|
+
const projectInfo = getSessionProject(sessionId);
|
|
1180
|
+
const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : 'lightweight_global';
|
|
1181
|
+
|
|
1182
|
+
if (!serviceCache.has(key)) {
|
|
1183
|
+
const storagePath = projectInfo
|
|
1184
|
+
? getProjectStoragePath(projectInfo.projectPath)
|
|
1185
|
+
: path.join(os.homedir(), '.claude-code', 'memory');
|
|
1186
|
+
|
|
1187
|
+
serviceCache.set(key, new MemoryService({
|
|
1188
|
+
storagePath,
|
|
1189
|
+
projectHash: projectInfo?.projectHash,
|
|
1190
|
+
lightweightMode: true, // Skip embedder/vector/workers
|
|
1191
|
+
analyticsEnabled: false,
|
|
1192
|
+
sharedStoreConfig: { enabled: false }
|
|
1193
|
+
}));
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return serviceCache.get(key)!;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1132
1199
|
export function createMemoryService(config: MemoryServiceConfig): MemoryService {
|
|
1133
1200
|
return new MemoryService(config);
|
|
1134
1201
|
}
|