claude-memory-layer 1.0.11 → 1.0.13
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
package/src/ui/app.js
CHANGED
|
@@ -11,6 +11,7 @@ const state = {
|
|
|
11
11
|
sharedStats: null,
|
|
12
12
|
mostAccessed: null,
|
|
13
13
|
helpfulness: null,
|
|
14
|
+
retrievalTraces: null,
|
|
14
15
|
currentLevel: 'L0',
|
|
15
16
|
currentSort: 'recent',
|
|
16
17
|
currentView: 'overview',
|
|
@@ -235,17 +236,19 @@ async function refreshData() {
|
|
|
235
236
|
if(btn) btn.classList.add('loading');
|
|
236
237
|
|
|
237
238
|
try {
|
|
238
|
-
const [stats, shared, mostAccessed, helpfulness] = await Promise.all([
|
|
239
|
+
const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
|
|
239
240
|
fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
|
|
240
241
|
fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
|
|
241
242
|
fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
|
|
242
|
-
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null)
|
|
243
|
+
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
|
|
244
|
+
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
|
|
243
245
|
]);
|
|
244
246
|
|
|
245
247
|
state.stats = stats;
|
|
246
248
|
state.sharedStats = shared;
|
|
247
249
|
state.mostAccessed = mostAccessed;
|
|
248
250
|
state.helpfulness = helpfulness;
|
|
251
|
+
state.retrievalTraces = retrievalTraces;
|
|
249
252
|
|
|
250
253
|
updateStatsUI();
|
|
251
254
|
updateSharedUI();
|
|
@@ -397,6 +400,7 @@ function updateMemoryUsageUI() {
|
|
|
397
400
|
updateGraduationBars();
|
|
398
401
|
updateHelpfulnessUI();
|
|
399
402
|
updateMostHelpfulList();
|
|
403
|
+
updateRetrievalTraceUI();
|
|
400
404
|
}
|
|
401
405
|
|
|
402
406
|
function updateGraduationBars() {
|
|
@@ -483,6 +487,69 @@ function updateMostHelpfulList() {
|
|
|
483
487
|
}).join('');
|
|
484
488
|
}
|
|
485
489
|
|
|
490
|
+
|
|
491
|
+
function updateRetrievalTraceUI() {
|
|
492
|
+
const summaryEl = document.getElementById('retrieval-trace-summary');
|
|
493
|
+
const listEl = document.getElementById('retrieval-trace-list');
|
|
494
|
+
if (!summaryEl || !listEl) return;
|
|
495
|
+
|
|
496
|
+
const payload = state.retrievalTraces;
|
|
497
|
+
const stats = payload?.stats;
|
|
498
|
+
const traces = payload?.traces || [];
|
|
499
|
+
|
|
500
|
+
if (!stats || !Number.isFinite(stats.totalQueries) || stats.totalQueries === 0) {
|
|
501
|
+
summaryEl.innerHTML = '<span style="color:var(--text-muted);">No retrieval traces yet.</span>';
|
|
502
|
+
listEl.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No query/context trace data</div>';
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const selectionRate = ((stats.selectionRate || 0) * 100).toFixed(1);
|
|
507
|
+
summaryEl.innerHTML = `
|
|
508
|
+
<div style="display:flex; gap:14px; flex-wrap:wrap; font-size:12px;">
|
|
509
|
+
<span><strong>${formatNumber(stats.totalQueries)}</strong> queries</span>
|
|
510
|
+
<span><strong>${Number(stats.avgCandidateCount || 0).toFixed(1)}</strong> avg candidates</span>
|
|
511
|
+
<span><strong>${Number(stats.avgSelectedCount || 0).toFixed(1)}</strong> avg selected</span>
|
|
512
|
+
<span><strong>${selectionRate}%</strong> selection rate</span>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
listEl.innerHTML = traces.slice(0, 8).map((t) => {
|
|
517
|
+
const ts = t.createdAt ? new Date(t.createdAt).toLocaleString() : '-';
|
|
518
|
+
const confidence = t.confidence || 'n/a';
|
|
519
|
+
const selected = Number(t.selectedCount || 0);
|
|
520
|
+
const candidates = Number(t.candidateCount || 0);
|
|
521
|
+
const selectedDetails = (t.selectedDetails || []).slice(0, 2);
|
|
522
|
+
const candidateDetails = (t.candidateDetails || []).slice(0, 3);
|
|
523
|
+
const selectedIdsHtml = selectedDetails.length > 0
|
|
524
|
+
? selectedDetails.map((d) => {
|
|
525
|
+
const breakdown = `score=${Number(d.score || 0).toFixed(3)} · s=${Number(d.semanticScore || 0).toFixed(3)} · l=${Number(d.lexicalScore || 0).toFixed(3)} · r=${Number(d.recencyScore || 0).toFixed(3)}`;
|
|
526
|
+
return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${d.eventId}')" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
|
|
527
|
+
}).join(' ')
|
|
528
|
+
: ((t.selectedEventIds || []).slice(0, 2).map((id) => `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${id}')">${escapeHtml((id || '').slice(0, 8))}...</span>`).join(' ') || '-');
|
|
529
|
+
|
|
530
|
+
const scoreBreakdownHtml = selectedDetails.length > 0
|
|
531
|
+
? selectedDetails.map((d) => `<div style="font-size:10px; color:var(--text-muted);">${escapeHtml((d.eventId || '').slice(0, 8))}... → score ${Number(d.score || 0).toFixed(3)} (s ${Number(d.semanticScore || 0).toFixed(3)}, l ${Number(d.lexicalScore || 0).toFixed(3)}, r ${Number(d.recencyScore || 0).toFixed(3)})</div>`).join('')
|
|
532
|
+
: '';
|
|
533
|
+
|
|
534
|
+
return `
|
|
535
|
+
<div class="shared-item" style="align-items:flex-start;">
|
|
536
|
+
<div class="shared-info" style="align-items:flex-start; flex-direction:column; gap:4px;">
|
|
537
|
+
<span style="font-size:12px; color:var(--text-secondary);"><strong>Q:</strong> ${escapeHtml((t.queryText || '').slice(0, 120))}</span>
|
|
538
|
+
<span style="font-size:11px; color:var(--text-muted);">${ts} · strategy=${escapeHtml(t.strategy || 'auto')} · conf=${escapeHtml(confidence)}</span>
|
|
539
|
+
<span style="font-size:11px; color:var(--text-muted);">selected IDs: ${selectedIdsHtml}</span>
|
|
540
|
+
<span style="font-size:11px; color:var(--text-muted);">candidates: ${candidateDetails.map((d) => `<span class=\"event-type-badge\" style=\"cursor:pointer;\" onclick=\"openDetailModal('${d.eventId}')\">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`).join(' ') || '-'}</span>
|
|
541
|
+
${scoreBreakdownHtml}
|
|
542
|
+
</div>
|
|
543
|
+
<div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:68px;">
|
|
544
|
+
<span style="font-size:13px; font-weight:600; color:var(--accent-primary);">${selected}/${candidates}</span>
|
|
545
|
+
<span style="font-size:10px; color:var(--text-muted);">sel/cand</span>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
`;
|
|
549
|
+
}).join('');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
486
553
|
// --- Charts ---
|
|
487
554
|
|
|
488
555
|
async function initActivityChart() {
|
package/src/ui/index.html
CHANGED
|
@@ -271,6 +271,14 @@
|
|
|
271
271
|
<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">Loading...</div>
|
|
272
272
|
</div>
|
|
273
273
|
</div>
|
|
274
|
+
|
|
275
|
+
<div style="margin-top:20px;">
|
|
276
|
+
<div class="section-label">Retrieval Trace (1:1)</div>
|
|
277
|
+
<div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
|
278
|
+
<div id="retrieval-trace-list" class="shared-list">
|
|
279
|
+
<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">Loading...</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
274
282
|
</div>
|
|
275
283
|
|
|
276
284
|
</div>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { bootstrapKnowledgeBase } from '../src/services/bootstrap-organizer.js';
|
|
7
|
+
|
|
8
|
+
async function makeTempRepo(): Promise<string> {
|
|
9
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'cml-bootstrap-'));
|
|
10
|
+
execSync('git init', { cwd: root, stdio: 'ignore' });
|
|
11
|
+
execSync('git config user.email test@example.com', { cwd: root, stdio: 'ignore' });
|
|
12
|
+
execSync('git config user.name test-bot', { cwd: root, stdio: 'ignore' });
|
|
13
|
+
|
|
14
|
+
await fs.mkdir(path.join(root, 'src'), { recursive: true });
|
|
15
|
+
await fs.mkdir(path.join(root, 'tests'), { recursive: true });
|
|
16
|
+
|
|
17
|
+
await fs.writeFile(path.join(root, 'src', 'index.ts'), 'export const hello = () => "world";\n', 'utf8');
|
|
18
|
+
await fs.writeFile(path.join(root, 'src', 'worker.ts'), 'export const runWorker = () => true;\n', 'utf8');
|
|
19
|
+
await fs.writeFile(path.join(root, 'tests', 'index.test.ts'), 'describe("x", () => {});\n', 'utf8');
|
|
20
|
+
|
|
21
|
+
execSync('git add .', { cwd: root, stdio: 'ignore' });
|
|
22
|
+
execSync('git commit -m "feat: initial project scaffolding"', { cwd: root, stdio: 'ignore' });
|
|
23
|
+
|
|
24
|
+
await fs.writeFile(path.join(root, 'src', 'worker.ts'), 'export const runWorker = () => "ok";\n', 'utf8');
|
|
25
|
+
execSync('git add src/worker.ts', { cwd: root, stdio: 'ignore' });
|
|
26
|
+
execSync('git commit -m "refactor: stabilize worker return type"', { cwd: root, stdio: 'ignore' });
|
|
27
|
+
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('bootstrapKnowledgeBase', () => {
|
|
32
|
+
it('generates structured bootstrap outputs with metadata', async () => {
|
|
33
|
+
const repo = await makeTempRepo();
|
|
34
|
+
const outDir = path.join(repo, '.generated-kb');
|
|
35
|
+
|
|
36
|
+
const result = await bootstrapKnowledgeBase({
|
|
37
|
+
repoPath: repo,
|
|
38
|
+
outDir,
|
|
39
|
+
since: '365 days ago',
|
|
40
|
+
maxCommits: 50
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.fileCount).toBeGreaterThan(0);
|
|
44
|
+
expect(result.commitCount).toBeGreaterThan(0);
|
|
45
|
+
|
|
46
|
+
const expected = [
|
|
47
|
+
path.join(outDir, 'overview', 'overview.md'),
|
|
48
|
+
path.join(outDir, 'modules', 'src.md'),
|
|
49
|
+
path.join(outDir, 'decisions', 'decisions.md'),
|
|
50
|
+
path.join(outDir, 'timeline', 'timeline.md'),
|
|
51
|
+
path.join(outDir, 'glossary', 'glossary.md'),
|
|
52
|
+
path.join(outDir, 'sources', 'manifest.md'),
|
|
53
|
+
path.join(outDir, 'sources', 'manifest.json')
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const file of expected) {
|
|
57
|
+
const stat = await fs.stat(file);
|
|
58
|
+
expect(stat.isFile()).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const overview = await fs.readFile(path.join(outDir, 'overview', 'overview.md'), 'utf8');
|
|
62
|
+
expect(overview).toContain('deterministicPipeline: true');
|
|
63
|
+
expect(overview).toContain('- confidence:');
|
|
64
|
+
expect(overview).toContain('- source:');
|
|
65
|
+
|
|
66
|
+
const decisions = await fs.readFile(path.join(outDir, 'decisions', 'decisions.md'), 'utf8');
|
|
67
|
+
expect(decisions).toContain('commit:');
|
|
68
|
+
|
|
69
|
+
const manifestJson = JSON.parse(await fs.readFile(path.join(outDir, 'sources', 'manifest.json'), 'utf8')) as {
|
|
70
|
+
deterministicPipeline: boolean;
|
|
71
|
+
outputs: string[];
|
|
72
|
+
};
|
|
73
|
+
expect(manifestJson.deterministicPipeline).toBe(true);
|
|
74
|
+
expect(manifestJson.outputs.length).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('supports incremental bootstrap mode with manifest baseline', async () => {
|
|
78
|
+
const repo = await makeTempRepo();
|
|
79
|
+
const outDir = path.join(repo, '.generated-kb');
|
|
80
|
+
|
|
81
|
+
await bootstrapKnowledgeBase({
|
|
82
|
+
repoPath: repo,
|
|
83
|
+
outDir,
|
|
84
|
+
since: '365 days ago',
|
|
85
|
+
maxCommits: 50,
|
|
86
|
+
incremental: false
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await fs.writeFile(path.join(repo, 'src', 'index.ts'), 'export const hello = () => "world2";\n', 'utf8');
|
|
90
|
+
execSync('git add src/index.ts', { cwd: repo, stdio: 'ignore' });
|
|
91
|
+
execSync('git commit -m "feat: change index output"', { cwd: repo, stdio: 'ignore' });
|
|
92
|
+
|
|
93
|
+
const result = await bootstrapKnowledgeBase({
|
|
94
|
+
repoPath: repo,
|
|
95
|
+
outDir,
|
|
96
|
+
maxCommits: 50,
|
|
97
|
+
incremental: true
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.commitCount).toBeGreaterThan(0);
|
|
101
|
+
|
|
102
|
+
const manifestJson = JSON.parse(await fs.readFile(path.join(outDir, 'sources', 'manifest.json'), 'utf8')) as {
|
|
103
|
+
mode: string;
|
|
104
|
+
options?: { incremental?: boolean };
|
|
105
|
+
stats?: { modulesGenerated?: number };
|
|
106
|
+
};
|
|
107
|
+
expect(manifestJson.mode).toBe('incremental');
|
|
108
|
+
expect(manifestJson.options?.incremental).toBe(true);
|
|
109
|
+
expect((manifestJson.stats?.modulesGenerated ?? 0)).toBeGreaterThan(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ConsolidationWorker } from '../src/core/consolidation-worker.js';
|
|
4
|
+
import type { EndlessModeConfig, MemoryEvent } from '../src/core/types.js';
|
|
5
|
+
|
|
6
|
+
function makeEvent(id: string, content: string, hoursAgo = 20): MemoryEvent {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
eventType: 'user_prompt',
|
|
10
|
+
sessionId: 's1',
|
|
11
|
+
timestamp: new Date(Date.now() - hoursAgo * 60 * 60 * 1000),
|
|
12
|
+
content,
|
|
13
|
+
canonicalKey: id,
|
|
14
|
+
dedupeKey: id,
|
|
15
|
+
metadata: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ConsolidationWorker hierarchy automation', () => {
|
|
20
|
+
it('creates consolidated memories, promotes rules, and returns cost-quality report', async () => {
|
|
21
|
+
const events = [
|
|
22
|
+
makeEvent('e1', 'implement auth bug fix and add tests for token refresh'),
|
|
23
|
+
makeEvent('e2', 'fix auth error in middleware and update tests'),
|
|
24
|
+
makeEvent('e3', 'auth feature update with regression test and bug notes'),
|
|
25
|
+
makeEvent('e4', 'implement retry logic for auth and fix issue with token cache'),
|
|
26
|
+
makeEvent('e5', 'add integration test for auth flow and bug reproduction'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const created: Array<{ memoryId: string; summary: string; topics: string[]; sourceEvents: string[]; confidence: number }> = [];
|
|
30
|
+
const rules: Array<{ rule: string; sourceMemoryIds: string[] }> = [];
|
|
31
|
+
|
|
32
|
+
const workingSetStore = {
|
|
33
|
+
async get() {
|
|
34
|
+
return { recentEvents: events, lastActivity: new Date(), continuityScore: 0.8 };
|
|
35
|
+
},
|
|
36
|
+
async prune(_ids: string[]) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const consolidatedStore = {
|
|
42
|
+
async isAlreadyConsolidated() { return false; },
|
|
43
|
+
async create(input: any) {
|
|
44
|
+
const memoryId = `m-${created.length + 1}`;
|
|
45
|
+
created.push({ memoryId, ...input });
|
|
46
|
+
return memoryId;
|
|
47
|
+
},
|
|
48
|
+
async get(memoryId: string) {
|
|
49
|
+
return created.find((m) => m.memoryId === memoryId) || null;
|
|
50
|
+
},
|
|
51
|
+
async hasRuleForSourceMemory() { return false; },
|
|
52
|
+
async createRule(input: any) {
|
|
53
|
+
rules.push({ rule: input.rule, sourceMemoryIds: input.sourceMemoryIds });
|
|
54
|
+
return `r-${rules.length}`;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const config: EndlessModeConfig = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
workingSet: { maxEvents: 100, timeWindowHours: 24, minRelevanceScore: 0.5 },
|
|
61
|
+
consolidation: { triggerIntervalMs: 3600000, triggerEventCount: 3, triggerIdleMs: 1000, useLLMSummarization: false },
|
|
62
|
+
continuity: { minScoreForSeamless: 0.7, topicDecayHours: 48 }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const worker = new ConsolidationWorker(workingSetStore as any, consolidatedStore as any, config);
|
|
66
|
+
const out = await worker.forceRunWithReport();
|
|
67
|
+
|
|
68
|
+
expect(out.consolidatedCount).toBeGreaterThan(0);
|
|
69
|
+
expect(out.promotedRuleCount).toBeGreaterThan(0);
|
|
70
|
+
expect(out.report.beforeTokenEstimate).toBeGreaterThan(0);
|
|
71
|
+
expect(out.report.reductionRatio).toBeGreaterThanOrEqual(0);
|
|
72
|
+
expect(out.report.qualityGuardPassed).toBe(true);
|
|
73
|
+
expect(rules[0]?.sourceMemoryIds?.length).toBeGreaterThan(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { IngestInterceptorRegistry, mergeHierarchicalMetadata } from '../src/core/ingest-interceptor.js';
|
|
3
|
+
|
|
4
|
+
describe('IngestInterceptorRegistry', () => {
|
|
5
|
+
it('runs before/after interceptors in staged order', async () => {
|
|
6
|
+
const registry = new IngestInterceptorRegistry();
|
|
7
|
+
const stages: string[] = [];
|
|
8
|
+
|
|
9
|
+
registry.registerBefore((ctx) => stages.push(`before:${ctx.operation}`));
|
|
10
|
+
registry.registerAfter((ctx) => stages.push(`after:${ctx.operation}`));
|
|
11
|
+
|
|
12
|
+
const event = {
|
|
13
|
+
eventType: 'user_prompt' as const,
|
|
14
|
+
sessionId: 's1',
|
|
15
|
+
timestamp: new Date(),
|
|
16
|
+
content: 'hello'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
await registry.run('before', { operation: 'user_prompt', sessionId: 's1', event });
|
|
20
|
+
await registry.run('after', { operation: 'user_prompt', sessionId: 's1', event });
|
|
21
|
+
|
|
22
|
+
expect(stages).toEqual(['before:user_prompt', 'after:user_prompt']);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('mergeHierarchicalMetadata', () => {
|
|
27
|
+
it('deep merges nested metadata without clobbering siblings', () => {
|
|
28
|
+
const merged = mergeHierarchicalMetadata(
|
|
29
|
+
{ scope: { project: { id: 'alpha', env: 'dev' } }, ingest: { source: 'hook' } },
|
|
30
|
+
{ scope: { project: { env: 'prod' }, turn: { id: 't1' } } }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(merged).toEqual({
|
|
34
|
+
scope: { project: { id: 'alpha', env: 'prod' }, turn: { id: 't1' } },
|
|
35
|
+
ingest: { source: 'hook' }
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { buildMirrorPath, MarkdownMirror } from '../src/core/markdown-mirror.js';
|
|
7
|
+
import { SQLiteEventStore } from '../src/core/sqlite-event-store.js';
|
|
8
|
+
|
|
9
|
+
describe('markdown mirror', () => {
|
|
10
|
+
it('builds sanitized path with defaults', () => {
|
|
11
|
+
const event = {
|
|
12
|
+
id: randomUUID(),
|
|
13
|
+
eventType: 'user_prompt' as const,
|
|
14
|
+
sessionId: 's1',
|
|
15
|
+
timestamp: new Date('2026-02-24T00:49:00.000Z'),
|
|
16
|
+
content: 'hello',
|
|
17
|
+
canonicalKey: 'k',
|
|
18
|
+
dedupeKey: 'd',
|
|
19
|
+
metadata: {
|
|
20
|
+
namespace: 'Team ../Prod',
|
|
21
|
+
categoryPath: ['Ops & Alerts', 'Night Shift']
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const out = buildMirrorPath('/tmp/root', event);
|
|
26
|
+
expect(out).toContain(path.join('memory', 'team-prod', 'ops-alerts', 'night-shift', '2026-02-24.md'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('appends without overwriting existing content', async () => {
|
|
30
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'cml-md-mirror-'));
|
|
31
|
+
try {
|
|
32
|
+
const file = path.join(root, 'memory', 'default', 'session_summary', '2026-02-24.md');
|
|
33
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
34
|
+
await fs.writeFile(file, 'PREEXISTING\n', 'utf8');
|
|
35
|
+
|
|
36
|
+
const mirror = new MarkdownMirror(root);
|
|
37
|
+
await mirror.append({
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
eventType: 'session_summary',
|
|
40
|
+
sessionId: 's2',
|
|
41
|
+
timestamp: new Date('2026-02-24T11:00:00.000Z'),
|
|
42
|
+
content: 'summary line',
|
|
43
|
+
canonicalKey: 'k2',
|
|
44
|
+
dedupeKey: 'd2',
|
|
45
|
+
metadata: {}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const content = await fs.readFile(file, 'utf8');
|
|
49
|
+
expect(content.startsWith('PREEXISTING\n')).toBe(true);
|
|
50
|
+
expect(content).toContain('summary line');
|
|
51
|
+
} finally {
|
|
52
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('is wired to sqlite append flow', async () => {
|
|
57
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'cml-md-flow-'));
|
|
58
|
+
try {
|
|
59
|
+
const store = new SQLiteEventStore(path.join(root, 'events.sqlite'), {
|
|
60
|
+
markdownMirrorRoot: root
|
|
61
|
+
});
|
|
62
|
+
const ts = new Date('2026-02-24T12:00:00.000Z');
|
|
63
|
+
const result = await store.append({
|
|
64
|
+
eventType: 'agent_response',
|
|
65
|
+
sessionId: 'sess-flow',
|
|
66
|
+
timestamp: ts,
|
|
67
|
+
content: 'flow content',
|
|
68
|
+
metadata: { namespace: 'app', category: 'responses' }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
|
|
73
|
+
// mirror append is async fire-and-forget; allow small delay
|
|
74
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
75
|
+
|
|
76
|
+
const file = path.join(root, 'memory', 'app', 'responses', '2026-02-24.md');
|
|
77
|
+
const content = await fs.readFile(file, 'utf8');
|
|
78
|
+
expect(content).toContain('flow content');
|
|
79
|
+
|
|
80
|
+
await store.close();
|
|
81
|
+
} finally {
|
|
82
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { buildMirrorPath, MarkdownMirror } from '../src/core/md-mirror.js';
|
|
6
|
+
import type { MemoryEventInput } from '../src/core/types.js';
|
|
7
|
+
|
|
8
|
+
function makeEvent(): MemoryEventInput {
|
|
9
|
+
return {
|
|
10
|
+
eventType: 'user_prompt',
|
|
11
|
+
sessionId: 'agent:main:test',
|
|
12
|
+
timestamp: new Date('2026-02-24T01:00:00.000Z'),
|
|
13
|
+
content: '아침 브리핑 포맷 기억해줘',
|
|
14
|
+
metadata: {
|
|
15
|
+
namespace: 'Briefing',
|
|
16
|
+
categoryPath: ['Preferences', 'Morning']
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('MarkdownMirror', () => {
|
|
22
|
+
it('builds sanitized categorized path', () => {
|
|
23
|
+
const p = buildMirrorPath('/tmp/demo', makeEvent());
|
|
24
|
+
expect(p).toContain(path.join('memory', 'briefing', 'preferences', 'morning', '2026-02-24.md'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses uncategorized when category is missing', () => {
|
|
28
|
+
const ev = { ...makeEvent(), metadata: { namespace: 'Briefing' } };
|
|
29
|
+
const p = buildMirrorPath('/tmp/demo', ev);
|
|
30
|
+
expect(p).toContain(path.join('memory', 'briefing', 'uncategorized', '2026-02-24.md'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('appends without overwrite', async () => {
|
|
34
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cl-md-mirror-'));
|
|
35
|
+
const mirror = new MarkdownMirror(tmp);
|
|
36
|
+
const ev = makeEvent();
|
|
37
|
+
|
|
38
|
+
await mirror.append(ev, 'e1');
|
|
39
|
+
await mirror.append({ ...ev, content: '두번째 기록' }, 'e2');
|
|
40
|
+
|
|
41
|
+
const out = buildMirrorPath(tmp, ev);
|
|
42
|
+
const text = fs.readFileSync(out, 'utf8');
|
|
43
|
+
expect(text).toContain('e1');
|
|
44
|
+
expect(text).toContain('e2');
|
|
45
|
+
expect(text).toContain('두번째 기록');
|
|
46
|
+
|
|
47
|
+
const index = fs.readFileSync(path.join(tmp, 'memory', '_index.md'), 'utf8');
|
|
48
|
+
expect(index).toContain('memory/briefing/preferences/morning/2026-02-24.md');
|
|
49
|
+
});
|
|
50
|
+
});
|