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.
@@ -1034,6 +1034,28 @@ var SQLiteEventStore = class {
1034
1034
  CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
1035
1035
  CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
1036
1036
  CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
1037
+
1038
+ -- FTS5 Full-Text Search for fast keyword search
1039
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
1040
+ content,
1041
+ event_id UNINDEXED,
1042
+ content='events',
1043
+ content_rowid='rowid'
1044
+ );
1045
+
1046
+ -- Triggers to keep FTS in sync with events table
1047
+ CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
1048
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1049
+ END;
1050
+
1051
+ CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
1052
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1053
+ END;
1054
+
1055
+ CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
1056
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1057
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1058
+ END;
1037
1059
  `);
1038
1060
  const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
1039
1061
  const columnNames = tableInfo.map((col) => col.name);
@@ -1474,6 +1496,62 @@ var SQLiteEventStore = class {
1474
1496
  );
1475
1497
  return rows.map((row) => this.rowToEvent(row));
1476
1498
  }
1499
+ /**
1500
+ * Fast keyword search using FTS5
1501
+ * Returns events matching the search query, ranked by relevance
1502
+ */
1503
+ async keywordSearch(query, limit = 10) {
1504
+ await this.initialize();
1505
+ const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
1506
+ if (!searchTerms) {
1507
+ return [];
1508
+ }
1509
+ try {
1510
+ const rows = sqliteAll(
1511
+ this.db,
1512
+ `SELECT e.*, fts.rank
1513
+ FROM events_fts fts
1514
+ JOIN events e ON e.id = fts.event_id
1515
+ WHERE events_fts MATCH ?
1516
+ ORDER BY fts.rank
1517
+ LIMIT ?`,
1518
+ [searchTerms, limit]
1519
+ );
1520
+ return rows.map((row) => ({
1521
+ event: this.rowToEvent(row),
1522
+ rank: row.rank
1523
+ }));
1524
+ } catch (error) {
1525
+ const likePattern = `%${query}%`;
1526
+ const rows = sqliteAll(
1527
+ this.db,
1528
+ `SELECT *, 0 as rank FROM events
1529
+ WHERE content LIKE ?
1530
+ ORDER BY timestamp DESC
1531
+ LIMIT ?`,
1532
+ [likePattern, limit]
1533
+ );
1534
+ return rows.map((row) => ({
1535
+ event: this.rowToEvent(row),
1536
+ rank: 0
1537
+ }));
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Rebuild FTS index from existing events
1542
+ * Call this once after upgrading to FTS5
1543
+ */
1544
+ async rebuildFtsIndex() {
1545
+ await this.initialize();
1546
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
1547
+ const totalEvents = countRow?.count ?? 0;
1548
+ sqliteExec(this.db, `
1549
+ DELETE FROM events_fts;
1550
+ INSERT INTO events_fts(rowid, content, event_id)
1551
+ SELECT rowid, content, id FROM events;
1552
+ `);
1553
+ return totalEvents;
1554
+ }
1477
1555
  /**
1478
1556
  * Get database instance for direct access
1479
1557
  */
@@ -4547,9 +4625,11 @@ var MemoryService = class {
4547
4625
  sharedStoreConfig = null;
4548
4626
  projectHash = null;
4549
4627
  readOnly;
4628
+ lightweightMode;
4550
4629
  constructor(config) {
4551
4630
  const storagePath = this.expandPath(config.storagePath);
4552
4631
  this.readOnly = config.readOnly ?? false;
4632
+ this.lightweightMode = config.lightweightMode ?? false;
4553
4633
  if (!this.readOnly && !fs.existsSync(storagePath)) {
4554
4634
  fs.mkdirSync(storagePath, { recursive: true });
4555
4635
  }
@@ -4596,6 +4676,10 @@ var MemoryService = class {
4596
4676
  if (this.initialized)
4597
4677
  return;
4598
4678
  await this.sqliteStore.initialize();
4679
+ if (this.lightweightMode) {
4680
+ this.initialized = true;
4681
+ return;
4682
+ }
4599
4683
  if (this.analyticsStore) {
4600
4684
  try {
4601
4685
  await this.analyticsStore.initialize();
@@ -4765,9 +4849,6 @@ var MemoryService = class {
4765
4849
  */
4766
4850
  async retrieveMemories(query, options) {
4767
4851
  await this.initialize();
4768
- if (this.vectorWorker) {
4769
- await this.vectorWorker.processAll();
4770
- }
4771
4852
  if (options?.includeShared && this.sharedStore) {
4772
4853
  return this.retriever.retrieveUnified(query, {
4773
4854
  ...options,
@@ -4777,6 +4858,29 @@ var MemoryService = class {
4777
4858
  }
4778
4859
  return this.retriever.retrieve(query, options);
4779
4860
  }
4861
+ /**
4862
+ * Fast keyword search using SQLite FTS5
4863
+ * Much faster than vector search - no embedding model needed
4864
+ */
4865
+ async keywordSearch(query, options) {
4866
+ await this.initialize();
4867
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
4868
+ const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
4869
+ const minRank = Math.max(...results.map((r) => r.rank), -1e3);
4870
+ const rankRange = maxRank - minRank || 1;
4871
+ return results.map((r) => ({
4872
+ event: r.event,
4873
+ score: 1 - (r.rank - minRank) / rankRange
4874
+ // Normalize to 0-1
4875
+ })).filter((r) => !options?.minScore || r.score >= options.minScore);
4876
+ }
4877
+ /**
4878
+ * Rebuild FTS index (call after database upgrade)
4879
+ */
4880
+ async rebuildFtsIndex() {
4881
+ await this.initialize();
4882
+ return this.sqliteStore.rebuildFtsIndex();
4883
+ }
4780
4884
  /**
4781
4885
  * Get session history
4782
4886
  */
@@ -5200,94 +5304,60 @@ var MemoryService = class {
5200
5304
  }
5201
5305
  };
5202
5306
  var serviceCache = /* @__PURE__ */ new Map();
5203
- var GLOBAL_KEY = "__global__";
5204
- function getDefaultMemoryService() {
5205
- if (!serviceCache.has(GLOBAL_KEY)) {
5206
- serviceCache.set(GLOBAL_KEY, new MemoryService({
5207
- storagePath: "~/.claude-code/memory",
5307
+ function getLightweightMemoryService(sessionId) {
5308
+ const projectInfo = getSessionProject(sessionId);
5309
+ const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : "lightweight_global";
5310
+ if (!serviceCache.has(key)) {
5311
+ const storagePath = projectInfo ? getProjectStoragePath(projectInfo.projectPath) : path.join(os.homedir(), ".claude-code", "memory");
5312
+ serviceCache.set(key, new MemoryService({
5313
+ storagePath,
5314
+ projectHash: projectInfo?.projectHash,
5315
+ lightweightMode: true,
5316
+ // Skip embedder/vector/workers
5208
5317
  analyticsEnabled: false,
5209
- // Hooks don't need DuckDB
5210
5318
  sharedStoreConfig: { enabled: false }
5211
- // Shared store uses DuckDB too
5212
5319
  }));
5213
5320
  }
5214
- return serviceCache.get(GLOBAL_KEY);
5215
- }
5216
- function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
5217
- const hash = hashProjectPath(projectPath);
5218
- if (!serviceCache.has(hash)) {
5219
- const storagePath = getProjectStoragePath(projectPath);
5220
- serviceCache.set(hash, new MemoryService({
5221
- storagePath,
5222
- projectHash: hash,
5223
- // Override shared store config - hooks don't need DuckDB
5224
- sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
5225
- analyticsEnabled: false
5226
- // Hooks don't need DuckDB
5227
- }));
5228
- }
5229
- return serviceCache.get(hash);
5230
- }
5231
- function getMemoryServiceForSession(sessionId) {
5232
- const projectInfo = getSessionProject(sessionId);
5233
- if (projectInfo) {
5234
- return getMemoryServiceForProject(projectInfo.projectPath);
5235
- }
5236
- return getDefaultMemoryService();
5321
+ return serviceCache.get(key);
5237
5322
  }
5238
5323
 
5239
5324
  // src/hooks/user-prompt-submit.ts
5325
+ var MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5");
5326
+ var MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.3");
5327
+ var ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== "false";
5240
5328
  async function main() {
5241
5329
  const inputData = await readStdin();
5242
5330
  const input = JSON.parse(inputData);
5243
- const memoryService = getMemoryServiceForSession(input.session_id);
5244
- const config = {
5245
- candidateCount: parseInt(process.env.CLAUDE_MEMORY_CANDIDATES || "10"),
5246
- minScore: parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || "0.7"),
5247
- maxMemories: parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || "5"),
5248
- dynamicThresholdRatio: parseFloat(process.env.CLAUDE_MEMORY_THRESHOLD_RATIO || "0.85"),
5249
- strictMinScore: parseFloat(process.env.CLAUDE_MEMORY_STRICT_MIN || "0.75")
5250
- };
5331
+ const memoryService = getLightweightMemoryService(input.session_id);
5251
5332
  try {
5252
- const includeShared = memoryService.isSharedStoreEnabled();
5253
- const retrievalResult = await memoryService.retrieveMemories(input.prompt, {
5254
- topK: config.candidateCount,
5255
- // Get more candidates initially
5256
- minScore: config.minScore,
5257
- includeShared
5258
- });
5259
5333
  await memoryService.storeUserPrompt(
5260
5334
  input.session_id,
5261
5335
  input.prompt
5262
5336
  );
5263
- let relevantMemories = [];
5264
- if (retrievalResult.memories && retrievalResult.memories.length > 0) {
5265
- const bestScore = Math.max(...retrievalResult.memories.map((m) => m.score));
5266
- const dynamicThreshold = Math.max(config.strictMinScore, bestScore * config.dynamicThresholdRatio);
5267
- relevantMemories = retrievalResult.memories.filter((m) => m.score >= dynamicThreshold);
5268
- relevantMemories = relevantMemories.slice(0, config.maxMemories);
5269
- if (relevantMemories.length === 0) {
5270
- const fallbackCount = Math.min(3, config.maxMemories);
5271
- relevantMemories = retrievalResult.memories.filter((m) => m.score >= config.minScore).slice(0, fallbackCount);
5272
- }
5273
- if (process.env.CLAUDE_MEMORY_DEBUG) {
5274
- console.error(`Memory filtering: ${retrievalResult.memories.length} candidates -> ${relevantMemories.length} selected`);
5275
- console.error(`Threshold: ${dynamicThreshold.toFixed(3)}, Best score: ${bestScore.toFixed(3)}`);
5276
- }
5277
- if (relevantMemories.length > 0) {
5278
- const eventIds = relevantMemories.map((m) => m.event.id);
5337
+ let context = "";
5338
+ if (ENABLE_SEARCH && input.prompt.length > 10) {
5339
+ const results = await memoryService.keywordSearch(input.prompt, {
5340
+ topK: MAX_MEMORIES,
5341
+ minScore: MIN_SCORE
5342
+ });
5343
+ if (results.length > 0) {
5344
+ const eventIds = results.map((r) => r.event.id);
5279
5345
  await memoryService.incrementMemoryAccess(eventIds);
5346
+ const memories = results.map((r) => {
5347
+ const preview = r.event.content.length > 300 ? r.event.content.substring(0, 300) + "..." : r.event.content;
5348
+ return `- [${r.event.eventType}] ${preview}`;
5349
+ });
5350
+ context = `\u{1F4A1} **Related memories found:**
5351
+
5352
+ ${memories.join("\n\n")}`;
5280
5353
  }
5281
5354
  }
5282
- const filteredResult = {
5283
- ...retrievalResult,
5284
- memories: relevantMemories
5285
- };
5286
- const context = memoryService.formatAsContext(filteredResult);
5287
5355
  const output = { context };
5288
5356
  console.log(JSON.stringify(output));
5289
5357
  } catch (error) {
5290
- console.error("Memory hook error:", error);
5358
+ if (process.env.CLAUDE_MEMORY_DEBUG) {
5359
+ console.error("Memory hook error:", error);
5360
+ }
5291
5361
  console.log(JSON.stringify({ context: "" }));
5292
5362
  }
5293
5363
  }