claude-memory-layer 1.0.19 → 1.0.21
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/index.js +182 -12
- package/dist/cli/index.js.map +2 -2
- package/dist/core/index.js +70 -10
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +155 -9
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +155 -9
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +155 -9
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +155 -9
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +155 -9
- package/dist/hooks/user-prompt-submit.js.map +2 -2
- package/dist/server/api/index.js +159 -9
- package/dist/server/api/index.js.map +2 -2
- package/dist/server/index.js +159 -9
- package/dist/server/index.js.map +2 -2
- package/dist/services/memory-service.js +155 -9
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +126 -0
- package/dist/ui/index.html +39 -0
- package/dist/ui/style.css +7 -0
- package/package.json +2 -2
- package/scripts/build.ts +1 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +25 -8
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/server/api/events.ts +6 -0
- package/src/services/memory-service.ts +112 -2
- package/src/ui/app.js +126 -0
- package/src/ui/index.html +39 -0
- package/src/ui/style.css +7 -0
package/dist/ui/app.js
CHANGED
|
@@ -14,6 +14,10 @@ const state = {
|
|
|
14
14
|
retrievalTraces: null,
|
|
15
15
|
adherenceSummary: null,
|
|
16
16
|
adherenceWindow: '24h',
|
|
17
|
+
userPromptSearchQuery: '',
|
|
18
|
+
userPromptItems: [],
|
|
19
|
+
userPromptPage: 1,
|
|
20
|
+
userPromptPageSize: 30,
|
|
17
21
|
currentLevel: 'L0',
|
|
18
22
|
currentSort: 'recent',
|
|
19
23
|
currentView: 'overview',
|
|
@@ -149,6 +153,39 @@ function setupEventListeners() {
|
|
|
149
153
|
searchInput.addEventListener('input', debounce((e) => handleSearch(e.target.value), 300));
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
// User prompt search
|
|
157
|
+
const userPromptSearch = document.getElementById('user-prompt-search');
|
|
158
|
+
if (userPromptSearch) {
|
|
159
|
+
userPromptSearch.addEventListener('input', debounce(async (e) => {
|
|
160
|
+
state.userPromptSearchQuery = e.target.value || '';
|
|
161
|
+
state.userPromptPage = 1;
|
|
162
|
+
await loadUserPromptsView();
|
|
163
|
+
}, 250));
|
|
164
|
+
}
|
|
165
|
+
const userPromptRefresh = document.getElementById('user-prompt-refresh');
|
|
166
|
+
if (userPromptRefresh) {
|
|
167
|
+
userPromptRefresh.addEventListener('click', async () => {
|
|
168
|
+
await loadUserPromptsView();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const userPromptPrev = document.getElementById('user-prompt-prev');
|
|
172
|
+
if (userPromptPrev) {
|
|
173
|
+
userPromptPrev.addEventListener('click', async () => {
|
|
174
|
+
if (state.userPromptPage <= 1) return;
|
|
175
|
+
state.userPromptPage -= 1;
|
|
176
|
+
await renderUserPromptList();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const userPromptNext = document.getElementById('user-prompt-next');
|
|
180
|
+
if (userPromptNext) {
|
|
181
|
+
userPromptNext.addEventListener('click', async () => {
|
|
182
|
+
const totalPages = Math.max(1, Math.ceil((state.userPromptItems?.length || 0) / state.userPromptPageSize));
|
|
183
|
+
if (state.userPromptPage >= totalPages) return;
|
|
184
|
+
state.userPromptPage += 1;
|
|
185
|
+
await renderUserPromptList();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
// Project selector
|
|
153
190
|
const projectSelect = document.getElementById('project-select');
|
|
154
191
|
if (projectSelect) {
|
|
@@ -1197,6 +1234,7 @@ function switchView(viewName) {
|
|
|
1197
1234
|
switch (viewName) {
|
|
1198
1235
|
case 'knowledge-graph': loadKnowledgeGraphView(); break;
|
|
1199
1236
|
case 'memory-banks': loadMemoryBanksView(); break;
|
|
1237
|
+
case 'user-prompts': loadUserPromptsView(); break;
|
|
1200
1238
|
case 'configuration': loadConfigurationView(); break;
|
|
1201
1239
|
}
|
|
1202
1240
|
}
|
|
@@ -1430,6 +1468,94 @@ async function loadMemoryBankLevel(level) {
|
|
|
1430
1468
|
}
|
|
1431
1469
|
}
|
|
1432
1470
|
|
|
1471
|
+
// --- User Prompts View ---
|
|
1472
|
+
|
|
1473
|
+
async function renderUserPromptList() {
|
|
1474
|
+
const listEl = document.getElementById('user-prompt-list');
|
|
1475
|
+
const pageEl = document.getElementById('user-prompt-page');
|
|
1476
|
+
const prevBtn = document.getElementById('user-prompt-prev');
|
|
1477
|
+
const nextBtn = document.getElementById('user-prompt-next');
|
|
1478
|
+
const metaEl = document.getElementById('user-prompt-meta');
|
|
1479
|
+
if (!listEl) return;
|
|
1480
|
+
|
|
1481
|
+
const items = state.userPromptItems || [];
|
|
1482
|
+
const pageSize = state.userPromptPageSize;
|
|
1483
|
+
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
1484
|
+
if (state.userPromptPage > totalPages) state.userPromptPage = totalPages;
|
|
1485
|
+
|
|
1486
|
+
const start = (state.userPromptPage - 1) * pageSize;
|
|
1487
|
+
const paged = items.slice(start, start + pageSize);
|
|
1488
|
+
|
|
1489
|
+
if (pageEl) pageEl.textContent = `${state.userPromptPage} / ${totalPages}`;
|
|
1490
|
+
if (prevBtn) prevBtn.disabled = state.userPromptPage <= 1;
|
|
1491
|
+
if (nextBtn) nextBtn.disabled = state.userPromptPage >= totalPages;
|
|
1492
|
+
|
|
1493
|
+
if (metaEl) {
|
|
1494
|
+
const sessionCount = new Set(items.map(i => i.sessionId)).size;
|
|
1495
|
+
metaEl.textContent = `${items.length} prompts · ${sessionCount} sessions${state.userPromptSearchQuery ? ` · query: "${state.userPromptSearchQuery}"` : ''}`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (paged.length === 0) {
|
|
1499
|
+
listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">No user prompts found.</div>';
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Group current page by session
|
|
1504
|
+
const groups = new Map();
|
|
1505
|
+
for (const e of paged) {
|
|
1506
|
+
const key = e.sessionId || 'unknown';
|
|
1507
|
+
const arr = groups.get(key) || [];
|
|
1508
|
+
arr.push(e);
|
|
1509
|
+
groups.set(key, arr);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const html = Array.from(groups.entries()).map(([sessionId, sessionItems]) => {
|
|
1513
|
+
const heading = `
|
|
1514
|
+
<div style="margin:10px 0 6px; font-size:12px; color:var(--text-muted); font-weight:600;">
|
|
1515
|
+
<i class="ri-chat-1-line"></i> Session ${escapeHtml((sessionId || '').slice(0, 16))}... · ${sessionItems.length} prompts
|
|
1516
|
+
</div>
|
|
1517
|
+
`;
|
|
1518
|
+
|
|
1519
|
+
const cards = sessionItems.map((e) => `
|
|
1520
|
+
<div class="event-item" style="cursor:pointer;" onclick="openDetailModal('${e.id}')">
|
|
1521
|
+
<div class="event-header">
|
|
1522
|
+
<span class="event-type-badge type-user-prompt">user_prompt</span>
|
|
1523
|
+
<span class="event-time">${new Date(e.timestamp).toLocaleString()}</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
<div class="event-content" style="-webkit-line-clamp:4;">${escapeHtml(e.preview || '')}</div>
|
|
1526
|
+
</div>
|
|
1527
|
+
`).join('');
|
|
1528
|
+
|
|
1529
|
+
return heading + cards;
|
|
1530
|
+
}).join('');
|
|
1531
|
+
|
|
1532
|
+
listEl.innerHTML = html;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
async function loadUserPromptsView() {
|
|
1536
|
+
const listEl = document.getElementById('user-prompt-list');
|
|
1537
|
+
if (!listEl) return;
|
|
1538
|
+
|
|
1539
|
+
listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">Loading user prompts...</div>';
|
|
1540
|
+
|
|
1541
|
+
try {
|
|
1542
|
+
const params = {
|
|
1543
|
+
type: 'user_prompt',
|
|
1544
|
+
sort: 'recent',
|
|
1545
|
+
limit: 500,
|
|
1546
|
+
q: state.userPromptSearchQuery || undefined
|
|
1547
|
+
};
|
|
1548
|
+
const res = await fetch(apiUrl(`${API_BASE}/events`, params));
|
|
1549
|
+
const data = await res.json();
|
|
1550
|
+
const items = data.events || [];
|
|
1551
|
+
state.userPromptItems = items;
|
|
1552
|
+
|
|
1553
|
+
await renderUserPromptList();
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
listEl.innerHTML = `<div style="padding:20px; text-align:center; color:var(--error);">Failed to load user prompts: ${escapeHtml(error.message)}</div>`;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1433
1559
|
// --- Configuration View ---
|
|
1434
1560
|
|
|
1435
1561
|
async function loadConfigurationView() {
|
package/dist/ui/index.html
CHANGED
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
<i class="ri-brain-line"></i>
|
|
52
52
|
<span>Memory Banks</span>
|
|
53
53
|
</li>
|
|
54
|
+
<li class="nav-item" data-nav="user-prompts">
|
|
55
|
+
<i class="ri-message-2-line"></i>
|
|
56
|
+
<span>User Prompts</span>
|
|
57
|
+
</li>
|
|
54
58
|
<li class="nav-item" data-nav="configuration">
|
|
55
59
|
<i class="ri-settings-4-line"></i>
|
|
56
60
|
<span>Configuration</span>
|
|
@@ -363,6 +367,41 @@
|
|
|
363
367
|
</div>
|
|
364
368
|
</div>
|
|
365
369
|
|
|
370
|
+
<!-- ========== VIEW: User Prompts ========== -->
|
|
371
|
+
<div id="view-user-prompts" class="page-view">
|
|
372
|
+
<header class="top-header">
|
|
373
|
+
<div class="page-title">
|
|
374
|
+
<h1>User Prompts</h1>
|
|
375
|
+
<p>Search and browse recent user prompts</p>
|
|
376
|
+
</div>
|
|
377
|
+
</header>
|
|
378
|
+
<div class="card" style="margin-bottom:16px;">
|
|
379
|
+
<div style="display:flex; gap:10px; align-items:center;">
|
|
380
|
+
<div class="search-wrapper" style="width:420px; max-width:100%;">
|
|
381
|
+
<i class="ri-search-line"></i>
|
|
382
|
+
<input type="text" id="user-prompt-search" class="search-input" placeholder="Search user prompts...">
|
|
383
|
+
</div>
|
|
384
|
+
<button id="user-prompt-refresh" class="btn btn-secondary"><i class="ri-refresh-line"></i><span>Refresh</span></button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="card">
|
|
388
|
+
<div class="card-header" style="align-items:flex-end;">
|
|
389
|
+
<div>
|
|
390
|
+
<div class="card-title"><i class="ri-history-line"></i><span>Latest User Prompt History</span></div>
|
|
391
|
+
<div id="user-prompt-meta" style="font-size:12px; color:var(--text-muted); margin-top:6px;"></div>
|
|
392
|
+
</div>
|
|
393
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
394
|
+
<button id="user-prompt-prev" class="sort-btn">Prev</button>
|
|
395
|
+
<span id="user-prompt-page" style="font-size:12px; color:var(--text-muted);">1 / 1</span>
|
|
396
|
+
<button id="user-prompt-next" class="sort-btn">Next</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
<div id="user-prompt-list" class="event-list">
|
|
400
|
+
<div style="padding:20px; text-align:center; color:var(--text-muted);">Loading...</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
366
405
|
<!-- ========== VIEW: Configuration ========== -->
|
|
367
406
|
<div id="view-configuration" class="page-view">
|
|
368
407
|
<header class="top-header">
|
package/dist/ui/style.css
CHANGED
|
@@ -502,8 +502,11 @@ body {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
.type-user { background: rgba(59, 130, 246, 0.2); color: #60A5FA; }
|
|
505
|
+
.type-user-prompt { background: rgba(59, 130, 246, 0.2); color: #60A5FA; }
|
|
505
506
|
.type-agent { background: rgba(16, 185, 129, 0.2); color: #34D399; }
|
|
507
|
+
.type-agent-response { background: rgba(16, 185, 129, 0.2); color: #34D399; }
|
|
506
508
|
.type-tool { background: rgba(245, 158, 11, 0.2); color: #FBBF24; }
|
|
509
|
+
.type-tool-observation { background: rgba(245, 158, 11, 0.2); color: #FBBF24; }
|
|
507
510
|
.type-system { background: rgba(139, 92, 246, 0.2); color: #A78BFA; }
|
|
508
511
|
|
|
509
512
|
.event-time {
|
|
@@ -597,6 +600,10 @@ body {
|
|
|
597
600
|
color: var(--accent-primary);
|
|
598
601
|
font-weight: 600;
|
|
599
602
|
}
|
|
603
|
+
.sort-btn:disabled {
|
|
604
|
+
opacity: 0.35;
|
|
605
|
+
cursor: not-allowed;
|
|
606
|
+
}
|
|
600
607
|
|
|
601
608
|
/* Access Badge */
|
|
602
609
|
.access-badge {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-layer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "Claude Code plugin that learns from conversations to provide personalized assistance",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@hono/node-server": "^1.13.0",
|
|
40
40
|
"@lancedb/lancedb": "^0.5.0",
|
|
41
|
-
"@
|
|
41
|
+
"@huggingface/transformers": "^3.8.1",
|
|
42
42
|
"better-sqlite3": "^12.6.2",
|
|
43
43
|
"commander": "^12.0.0",
|
|
44
44
|
"duckdb": "^0.10.0",
|
package/scripts/build.ts
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -814,6 +814,7 @@ program
|
|
|
814
814
|
.option('-a, --all', 'Import all sessions from all projects')
|
|
815
815
|
.option('-l, --limit <number>', 'Limit messages per session')
|
|
816
816
|
.option('-f, --force', 'Force reimport: delete existing events and reimport with turn_id grouping')
|
|
817
|
+
.option('--embedding-model <name>', 'Embedding model override (default: jinaai/jina-embeddings-v5-text-nano-text-matching, or env CLAUDE_MEMORY_EMBEDDING_MODEL; fallback env: CLAUDE_MEMORY_EMBEDDING_FALLBACK_MODEL)')
|
|
817
818
|
.option('-v, --verbose', 'Show detailed progress')
|
|
818
819
|
.action(async (options) => {
|
|
819
820
|
const startTime = Date.now();
|
|
@@ -821,6 +822,10 @@ program
|
|
|
821
822
|
// Determine target project path for storage
|
|
822
823
|
const targetProjectPath = options.project || process.cwd();
|
|
823
824
|
|
|
825
|
+
if (options.embeddingModel) {
|
|
826
|
+
process.env.CLAUDE_MEMORY_EMBEDDING_MODEL = options.embeddingModel;
|
|
827
|
+
}
|
|
828
|
+
|
|
824
829
|
// Use project-specific memory service
|
|
825
830
|
const service = getMemoryServiceForProject(targetProjectPath);
|
|
826
831
|
const importer = createSessionHistoryImporter(service);
|
|
@@ -835,7 +840,16 @@ program
|
|
|
835
840
|
try {
|
|
836
841
|
console.log('\n⏳ Initializing memory service...');
|
|
837
842
|
await service.initialize();
|
|
838
|
-
console.log(
|
|
843
|
+
console.log(` ✅ Ready (embedder: ${service.getEmbeddingModelName()})\n`);
|
|
844
|
+
|
|
845
|
+
const migration = await service.ensureEmbeddingModelForImport({ autoMigrate: true });
|
|
846
|
+
if (migration.changed) {
|
|
847
|
+
console.log('🔁 Embedding model migration detected/required');
|
|
848
|
+
console.log(` Previous: ${migration.previousModel || 'legacy-unknown'}`);
|
|
849
|
+
console.log(` Current: ${migration.currentModel}`);
|
|
850
|
+
console.log(` Re-queued embeddings: ${migration.enqueued}`);
|
|
851
|
+
console.log(' (Import will continue and process embeddings with the new model)\n');
|
|
852
|
+
}
|
|
839
853
|
|
|
840
854
|
if (options.force) {
|
|
841
855
|
console.log('🔄 Force mode: existing events will be deleted and reimported with turn_id grouping\n');
|
|
@@ -862,6 +876,14 @@ program
|
|
|
862
876
|
const globalService = getDefaultMemoryService();
|
|
863
877
|
const globalImporter = createSessionHistoryImporter(globalService);
|
|
864
878
|
await globalService.initialize();
|
|
879
|
+
console.log(` ✅ Global service ready (embedder: ${globalService.getEmbeddingModelName()})`);
|
|
880
|
+
const globalMigration = await globalService.ensureEmbeddingModelForImport({ autoMigrate: true });
|
|
881
|
+
if (globalMigration.changed) {
|
|
882
|
+
console.log('🔁 Global embedding migration detected');
|
|
883
|
+
console.log(` Previous: ${globalMigration.previousModel || 'legacy-unknown'}`);
|
|
884
|
+
console.log(` Current: ${globalMigration.currentModel}`);
|
|
885
|
+
console.log(` Re-queued embeddings: ${globalMigration.enqueued}`);
|
|
886
|
+
}
|
|
865
887
|
result = await globalImporter.importAll(importOpts);
|
|
866
888
|
|
|
867
889
|
// Process embeddings
|
package/src/core/embedder.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* AXIOMMIND Principle 7: Standard JSON format for vectors
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { pipeline, Pipeline } from '@
|
|
6
|
+
import { pipeline, Pipeline } from '@huggingface/transformers';
|
|
7
7
|
|
|
8
8
|
export interface EmbeddingResult {
|
|
9
9
|
vector: number[];
|
|
@@ -14,10 +14,12 @@ export interface EmbeddingResult {
|
|
|
14
14
|
export class Embedder {
|
|
15
15
|
private pipeline: Pipeline | null = null;
|
|
16
16
|
private readonly modelName: string;
|
|
17
|
+
private activeModelName: string;
|
|
17
18
|
private initialized = false;
|
|
18
19
|
|
|
19
|
-
constructor(modelName: string = '
|
|
20
|
+
constructor(modelName: string = 'jinaai/jina-embeddings-v5-text-nano-text-matching') {
|
|
20
21
|
this.modelName = modelName;
|
|
22
|
+
this.activeModelName = modelName;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -26,8 +28,22 @@ export class Embedder {
|
|
|
26
28
|
async initialize(): Promise<void> {
|
|
27
29
|
if (this.initialized) return;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
try {
|
|
32
|
+
this.pipeline = await pipeline('feature-extraction', this.modelName);
|
|
33
|
+
this.activeModelName = this.modelName;
|
|
34
|
+
this.initialized = true;
|
|
35
|
+
return;
|
|
36
|
+
} catch (primaryError) {
|
|
37
|
+
const fallbackModel = process.env.CLAUDE_MEMORY_EMBEDDING_FALLBACK_MODEL || 'onnx-community/embeddinggemma-300m-ONNX';
|
|
38
|
+
if (fallbackModel === this.modelName) {
|
|
39
|
+
throw primaryError;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.warn(`[Embedder] Primary model failed (${this.modelName}). Falling back to ${fallbackModel}`);
|
|
43
|
+
this.pipeline = await pipeline('feature-extraction', fallbackModel);
|
|
44
|
+
this.activeModelName = fallbackModel;
|
|
45
|
+
this.initialized = true;
|
|
46
|
+
}
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
/**
|
|
@@ -49,7 +65,7 @@ export class Embedder {
|
|
|
49
65
|
|
|
50
66
|
return {
|
|
51
67
|
vector,
|
|
52
|
-
model: this.
|
|
68
|
+
model: this.activeModelName,
|
|
53
69
|
dimensions: vector.length
|
|
54
70
|
};
|
|
55
71
|
}
|
|
@@ -81,7 +97,7 @@ export class Embedder {
|
|
|
81
97
|
|
|
82
98
|
results.push({
|
|
83
99
|
vector,
|
|
84
|
-
model: this.
|
|
100
|
+
model: this.activeModelName,
|
|
85
101
|
dimensions: vector.length
|
|
86
102
|
});
|
|
87
103
|
}
|
|
@@ -109,7 +125,7 @@ export class Embedder {
|
|
|
109
125
|
* Get model name
|
|
110
126
|
*/
|
|
111
127
|
getModelName(): string {
|
|
112
|
-
return this.
|
|
128
|
+
return this.activeModelName;
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -117,8 +133,9 @@ export class Embedder {
|
|
|
117
133
|
let defaultEmbedder: Embedder | null = null;
|
|
118
134
|
|
|
119
135
|
export function getDefaultEmbedder(): Embedder {
|
|
136
|
+
const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
120
137
|
if (!defaultEmbedder) {
|
|
121
|
-
defaultEmbedder = new Embedder();
|
|
138
|
+
defaultEmbedder = new Embedder(envModel || undefined);
|
|
122
139
|
}
|
|
123
140
|
return defaultEmbedder;
|
|
124
141
|
}
|
|
@@ -865,6 +865,38 @@ export class SQLiteEventStore {
|
|
|
865
865
|
);
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Clear embedding outbox (used for embedding model migration)
|
|
870
|
+
*/
|
|
871
|
+
async clearEmbeddingOutbox(): Promise<void> {
|
|
872
|
+
await this.initialize();
|
|
873
|
+
sqliteRun(this.db, `DELETE FROM embedding_outbox`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Count total events
|
|
878
|
+
*/
|
|
879
|
+
async countEvents(): Promise<number> {
|
|
880
|
+
await this.initialize();
|
|
881
|
+
const row = sqliteGet<{ count: number }>(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
882
|
+
return row?.count || 0;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
887
|
+
*/
|
|
888
|
+
async getEventsPage(limit: number = 1000, offset: number = 0): Promise<MemoryEvent[]> {
|
|
889
|
+
await this.initialize();
|
|
890
|
+
|
|
891
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
892
|
+
this.db,
|
|
893
|
+
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
894
|
+
[limit, offset]
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
return rows.map(this.rowToEvent);
|
|
898
|
+
}
|
|
899
|
+
|
|
868
900
|
/**
|
|
869
901
|
* Mark outbox items as failed
|
|
870
902
|
*/
|
package/src/core/types.ts
CHANGED
|
@@ -152,8 +152,8 @@ export const ConfigSchema = z.object({
|
|
|
152
152
|
}).default({}),
|
|
153
153
|
embedding: z.object({
|
|
154
154
|
provider: z.enum(['local', 'openai']).default('local'),
|
|
155
|
-
model: z.string().default('
|
|
156
|
-
openaiModel: z.string().default('text-
|
|
155
|
+
model: z.string().default('jinaai/jina-embeddings-v5-text-nano-text-matching'),
|
|
156
|
+
openaiModel: z.string().default('jinaai/jina-embeddings-v5-text-nano-text-matching'),
|
|
157
157
|
batchSize: z.number().default(32)
|
|
158
158
|
}).default({}),
|
|
159
159
|
retrieval: z.object({
|
package/src/core/vector-store.ts
CHANGED
|
@@ -194,6 +194,26 @@ export class VectorStore {
|
|
|
194
194
|
return result;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Clear all vectors (used for embedding model migration)
|
|
199
|
+
*/
|
|
200
|
+
async clearAll(): Promise<void> {
|
|
201
|
+
await this.initialize();
|
|
202
|
+
if (!this.db) return;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
if (typeof (this.db as any).dropTable === 'function') {
|
|
206
|
+
await (this.db as any).dropTable(this.tableName);
|
|
207
|
+
} else if (typeof (this.db as any).drop_table === 'function') {
|
|
208
|
+
await (this.db as any).drop_table(this.tableName);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Ignore if table does not exist
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.table = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
/**
|
|
198
218
|
* Check if vector exists for event
|
|
199
219
|
*/
|
package/src/server/api/events.ts
CHANGED
|
@@ -14,6 +14,7 @@ eventsRouter.get('/', async (c) => {
|
|
|
14
14
|
const eventType = c.req.query('type');
|
|
15
15
|
const level = c.req.query('level');
|
|
16
16
|
const sort = c.req.query('sort') || 'recent'; // recent | accessed | oldest
|
|
17
|
+
const q = (c.req.query('q') || '').trim().toLowerCase();
|
|
17
18
|
const limit = parseInt(c.req.query('limit') || '100', 10);
|
|
18
19
|
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
19
20
|
const memoryService = getServiceFromQuery(c);
|
|
@@ -40,6 +41,11 @@ eventsRouter.get('/', async (c) => {
|
|
|
40
41
|
events = events.filter(e => e.eventType === eventType);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Content query filter
|
|
45
|
+
if (q) {
|
|
46
|
+
events = events.filter(e => (e.content || '').toLowerCase().includes(q));
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
// Sort
|
|
44
50
|
if (sort === 'accessed') {
|
|
45
51
|
events.sort((a: any, b: any) => {
|
|
@@ -213,9 +213,11 @@ export class MemoryService {
|
|
|
213
213
|
private readonly readOnly: boolean;
|
|
214
214
|
private readonly lightweightMode: boolean;
|
|
215
215
|
private readonly mdMirror: MarkdownMirror;
|
|
216
|
+
private readonly storagePath: string;
|
|
216
217
|
|
|
217
218
|
constructor(config: MemoryServiceConfig & { projectHash?: string; projectPath?: string; sharedStoreConfig?: SharedStoreConfig }) {
|
|
218
219
|
const storagePath = this.expandPath(config.storagePath);
|
|
220
|
+
this.storagePath = storagePath;
|
|
219
221
|
this.readOnly = config.readOnly ?? false;
|
|
220
222
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
221
223
|
this.mdMirror = new MarkdownMirror(process.cwd());
|
|
@@ -268,8 +270,9 @@ export class MemoryService {
|
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
274
|
+
this.embedder = embeddingModel
|
|
275
|
+
? new Embedder(embeddingModel)
|
|
273
276
|
: getDefaultEmbedder();
|
|
274
277
|
this.matcher = getDefaultMatcher();
|
|
275
278
|
// Retriever uses SQLite as primary (always available)
|
|
@@ -1474,6 +1477,113 @@ export class MemoryService {
|
|
|
1474
1477
|
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
1475
1478
|
}
|
|
1476
1479
|
|
|
1480
|
+
getEmbeddingModelName(): string {
|
|
1481
|
+
return this.embedder.getModelName();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Ensure embedding model metadata is in sync and optionally migrate vectors.
|
|
1486
|
+
* Migration strategy: clear vector index + clear embedding outbox + re-enqueue all events.
|
|
1487
|
+
*/
|
|
1488
|
+
async ensureEmbeddingModelForImport(options?: { autoMigrate?: boolean }): Promise<{
|
|
1489
|
+
changed: boolean;
|
|
1490
|
+
previousModel: string | null;
|
|
1491
|
+
currentModel: string;
|
|
1492
|
+
enqueued: number;
|
|
1493
|
+
reason?: string;
|
|
1494
|
+
}> {
|
|
1495
|
+
await this.initialize();
|
|
1496
|
+
|
|
1497
|
+
const currentModel = this.getEmbeddingModelName();
|
|
1498
|
+
const metaPath = path.join(this.storagePath, 'embedding-meta.json');
|
|
1499
|
+
|
|
1500
|
+
let previousModel: string | null = null;
|
|
1501
|
+
try {
|
|
1502
|
+
if (fs.existsSync(metaPath)) {
|
|
1503
|
+
const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) as { model?: string };
|
|
1504
|
+
previousModel = parsed?.model || null;
|
|
1505
|
+
}
|
|
1506
|
+
} catch {
|
|
1507
|
+
previousModel = null;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const stats = await this.getStats();
|
|
1511
|
+
const hasExistingVectors = (stats.vectorCount || 0) > 0;
|
|
1512
|
+
|
|
1513
|
+
// First-time metadata write (no migration needed unless legacy vectors exist)
|
|
1514
|
+
if (!previousModel && !hasExistingVectors) {
|
|
1515
|
+
fs.writeFileSync(metaPath, JSON.stringify({ model: currentModel, updatedAt: new Date().toISOString() }, null, 2));
|
|
1516
|
+
return { changed: false, previousModel: null, currentModel, enqueued: 0, reason: 'initialized-meta' };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const modelChanged = previousModel !== currentModel;
|
|
1520
|
+
const legacyUnknownButVectorsExist = !previousModel && hasExistingVectors;
|
|
1521
|
+
|
|
1522
|
+
if (!modelChanged && !legacyUnknownButVectorsExist) {
|
|
1523
|
+
return { changed: false, previousModel, currentModel, enqueued: 0 };
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (options?.autoMigrate === false) {
|
|
1527
|
+
return {
|
|
1528
|
+
changed: true,
|
|
1529
|
+
previousModel,
|
|
1530
|
+
currentModel,
|
|
1531
|
+
enqueued: 0,
|
|
1532
|
+
reason: legacyUnknownButVectorsExist ? 'legacy-vectors-without-meta' : 'model-mismatch'
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Pause background vector processing while preparing migration
|
|
1537
|
+
const wasRunning = this.vectorWorker?.isRunning() || false;
|
|
1538
|
+
if (wasRunning) this.vectorWorker?.stop();
|
|
1539
|
+
|
|
1540
|
+
// Reset vector and outbox state
|
|
1541
|
+
await this.vectorStore.clearAll();
|
|
1542
|
+
await this.sqliteStore.clearEmbeddingOutbox();
|
|
1543
|
+
|
|
1544
|
+
// Re-enqueue all events for new embeddings
|
|
1545
|
+
const pageSize = 1000;
|
|
1546
|
+
let offset = 0;
|
|
1547
|
+
let enqueued = 0;
|
|
1548
|
+
|
|
1549
|
+
while (true) {
|
|
1550
|
+
const page = await this.sqliteStore.getEventsPage(pageSize, offset);
|
|
1551
|
+
if (page.length === 0) break;
|
|
1552
|
+
|
|
1553
|
+
for (const event of page) {
|
|
1554
|
+
await this.sqliteStore.enqueueForEmbedding(event.id, event.content);
|
|
1555
|
+
enqueued += 1;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
offset += page.length;
|
|
1559
|
+
if (page.length < pageSize) break;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
fs.writeFileSync(
|
|
1563
|
+
metaPath,
|
|
1564
|
+
JSON.stringify(
|
|
1565
|
+
{
|
|
1566
|
+
model: currentModel,
|
|
1567
|
+
previousModel,
|
|
1568
|
+
migratedAt: new Date().toISOString(),
|
|
1569
|
+
enqueued
|
|
1570
|
+
},
|
|
1571
|
+
null,
|
|
1572
|
+
2
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1575
|
+
|
|
1576
|
+
if (wasRunning) this.vectorWorker?.start();
|
|
1577
|
+
|
|
1578
|
+
return {
|
|
1579
|
+
changed: true,
|
|
1580
|
+
previousModel,
|
|
1581
|
+
currentModel,
|
|
1582
|
+
enqueued,
|
|
1583
|
+
reason: legacyUnknownButVectorsExist ? 'legacy-vectors-without-meta' : 'model-mismatch'
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1477
1587
|
/**
|
|
1478
1588
|
* Backward-compatible alias used by some hooks
|
|
1479
1589
|
*/
|