@rlabs-inc/memory 0.1.0 → 0.2.1
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/index.js +130 -18
- package/dist/index.mjs +130 -18
- package/dist/server/index.js +141 -18
- package/dist/server/index.mjs +141 -18
- package/hooks/curation.ts +74 -0
- package/hooks/session-start.ts +82 -0
- package/hooks/user-prompt.ts +81 -0
- package/package.json +14 -8
- package/src/cli/colors.ts +174 -0
- package/src/cli/commands/doctor.ts +143 -0
- package/src/cli/commands/install.ts +153 -0
- package/src/cli/commands/serve.ts +76 -0
- package/src/cli/commands/stats.ts +64 -0
- package/src/cli/index.ts +128 -0
- package/src/core/curator.ts +7 -48
- package/src/core/engine.test.ts +321 -0
- package/src/core/engine.ts +45 -8
- package/src/core/retrieval.ts +1 -1
- package/src/core/store.ts +109 -98
- package/src/server/index.ts +15 -40
- package/src/types/schema.ts +1 -1
- package/src/utils/logger.ts +158 -107
- package/bun.lock +0 -102
- package/test-retrieval.ts +0 -91
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// STATS COMMAND - Show memory statistics
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { c, symbols, fmt } from '../colors.ts'
|
|
6
|
+
|
|
7
|
+
interface StatsOptions {
|
|
8
|
+
project?: string
|
|
9
|
+
verbose?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MEMORY_API_URL = process.env.MEMORY_API_URL || 'http://localhost:8765'
|
|
13
|
+
|
|
14
|
+
export async function stats(options: StatsOptions) {
|
|
15
|
+
console.log()
|
|
16
|
+
console.log(c.header(`${symbols.brain} Memory Statistics`))
|
|
17
|
+
console.log()
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Check if server is running
|
|
21
|
+
const healthResponse = await fetch(`${MEMORY_API_URL}/health`).catch(() => null)
|
|
22
|
+
|
|
23
|
+
if (!healthResponse?.ok) {
|
|
24
|
+
console.log(` ${c.error(symbols.cross)} Memory server is not running`)
|
|
25
|
+
console.log()
|
|
26
|
+
console.log(c.muted(` Start it with: memory serve`))
|
|
27
|
+
console.log()
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const health = await healthResponse.json()
|
|
32
|
+
console.log(` ${c.success(symbols.tick)} Server: ${c.cyan(MEMORY_API_URL)}`)
|
|
33
|
+
console.log(` ${fmt.kv('Engine', health.engine || 'unknown')}`)
|
|
34
|
+
console.log()
|
|
35
|
+
|
|
36
|
+
// Get project stats if project specified
|
|
37
|
+
if (options.project) {
|
|
38
|
+
const statsUrl = `${MEMORY_API_URL}/memory/stats?project_id=${encodeURIComponent(options.project)}`
|
|
39
|
+
const statsResponse = await fetch(statsUrl)
|
|
40
|
+
|
|
41
|
+
if (statsResponse.ok) {
|
|
42
|
+
const projectStats = await statsResponse.json()
|
|
43
|
+
console.log(fmt.section(`Project: ${options.project}`))
|
|
44
|
+
console.log()
|
|
45
|
+
console.log(` ${fmt.kv('Total Memories', projectStats.totalMemories || 0)}`)
|
|
46
|
+
console.log(` ${fmt.kv('Total Sessions', projectStats.totalSessions || 0)}`)
|
|
47
|
+
console.log(` ${fmt.kv('Stale Memories', projectStats.staleMemories || 0)}`)
|
|
48
|
+
if (projectStats.latestSession) {
|
|
49
|
+
console.log(` ${fmt.kv('Latest Session', projectStats.latestSession.slice(0, 8) + '...')}`)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
console.log(c.warn(` ${symbols.warning} Could not fetch stats for project: ${options.project}`))
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.log(c.muted(` Use --project <name> to see project-specific stats`))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log()
|
|
59
|
+
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
console.error(c.error(`${symbols.cross} Error: ${error.message}`))
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// MEMORY CLI - Beautiful command-line interface
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { parseArgs } from 'util'
|
|
7
|
+
import { c, symbols, fmt } from './colors.ts'
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Show help message
|
|
13
|
+
*/
|
|
14
|
+
function showHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
${c.header(`${symbols.brain} Memory`)} ${c.muted(`v${VERSION}`)}
|
|
17
|
+
${c.muted('Consciousness continuity for Claude')}
|
|
18
|
+
|
|
19
|
+
${c.bold('Usage:')}
|
|
20
|
+
${fmt.cmd('memory <command> [options]')}
|
|
21
|
+
|
|
22
|
+
${c.bold('Commands:')}
|
|
23
|
+
${c.command('serve')} Start the memory server ${c.muted('(default)')}
|
|
24
|
+
${c.command('stats')} Show memory statistics
|
|
25
|
+
${c.command('install')} Set up Claude Code hooks
|
|
26
|
+
${c.command('doctor')} Check system health
|
|
27
|
+
${c.command('help')} Show this help message
|
|
28
|
+
|
|
29
|
+
${c.bold('Options:')}
|
|
30
|
+
${c.cyan('-p, --port')} <port> Server port ${c.muted('(default: 8765)')}
|
|
31
|
+
${c.cyan('-v, --verbose')} Verbose output
|
|
32
|
+
${c.cyan('-q, --quiet')} Minimal output
|
|
33
|
+
${c.cyan('--version')} Show version
|
|
34
|
+
|
|
35
|
+
${c.bold('Examples:')}
|
|
36
|
+
${fmt.cmd('memory')} ${c.muted('# Start server on default port')}
|
|
37
|
+
${fmt.cmd('memory serve --port 9000')} ${c.muted('# Start on custom port')}
|
|
38
|
+
${fmt.cmd('memory stats')} ${c.muted('# Show memory statistics')}
|
|
39
|
+
${fmt.cmd('memory install')} ${c.muted('# Set up hooks for Claude Code')}
|
|
40
|
+
|
|
41
|
+
${c.muted('Documentation: https://github.com/RLabs-Inc/memory')}
|
|
42
|
+
`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Show version
|
|
47
|
+
*/
|
|
48
|
+
function showVersion() {
|
|
49
|
+
console.log(`${symbols.brain} memory v${VERSION}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Main entry point
|
|
54
|
+
*/
|
|
55
|
+
async function main() {
|
|
56
|
+
const { values, positionals } = parseArgs({
|
|
57
|
+
args: Bun.argv.slice(2),
|
|
58
|
+
options: {
|
|
59
|
+
port: { type: 'string', short: 'p', default: '8765' },
|
|
60
|
+
verbose: { type: 'boolean', short: 'v', default: false },
|
|
61
|
+
quiet: { type: 'boolean', short: 'q', default: false },
|
|
62
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
63
|
+
version: { type: 'boolean', default: false },
|
|
64
|
+
},
|
|
65
|
+
allowPositionals: true,
|
|
66
|
+
strict: false, // Allow unknown options for subcommands
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Handle global flags
|
|
70
|
+
if (values.version) {
|
|
71
|
+
showVersion()
|
|
72
|
+
process.exit(0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const command = positionals[0] || 'serve'
|
|
76
|
+
|
|
77
|
+
if (values.help && command === 'serve') {
|
|
78
|
+
showHelp()
|
|
79
|
+
process.exit(0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Route to commands
|
|
83
|
+
switch (command) {
|
|
84
|
+
case 'serve':
|
|
85
|
+
case 'start':
|
|
86
|
+
case 'run': {
|
|
87
|
+
const { serve } = await import('./commands/serve.ts')
|
|
88
|
+
await serve(values)
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'stats':
|
|
93
|
+
case 'status': {
|
|
94
|
+
const { stats } = await import('./commands/stats.ts')
|
|
95
|
+
await stats(values)
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'install':
|
|
100
|
+
case 'setup': {
|
|
101
|
+
const { install } = await import('./commands/install.ts')
|
|
102
|
+
await install(values)
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'doctor':
|
|
107
|
+
case 'check': {
|
|
108
|
+
const { doctor } = await import('./commands/doctor.ts')
|
|
109
|
+
await doctor(values)
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'help':
|
|
114
|
+
showHelp()
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
console.error(c.error(`Unknown command: ${command}`))
|
|
119
|
+
console.log(c.muted(`Run 'memory help' for usage information`))
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Run
|
|
125
|
+
main().catch(err => {
|
|
126
|
+
console.error(c.error(`Error: ${err.message}`))
|
|
127
|
+
process.exit(1)
|
|
128
|
+
})
|
package/src/core/curator.ts
CHANGED
|
@@ -70,8 +70,6 @@ export class Curator {
|
|
|
70
70
|
cliType: config.cliType ?? 'claude-code',
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Log which CLI we're using
|
|
74
|
-
console.log(`🧠 Curator initialized with CLI: ${cliCommand}`)
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
/**
|
|
@@ -154,8 +152,9 @@ Return ONLY this JSON structure:
|
|
|
154
152
|
"interaction_tone": "The tone/style of interaction (e.g., professional and focused, warm collaborative friendship, mentor-student dynamic, casual technical discussion, or null if neutral)",
|
|
155
153
|
"project_snapshot": {
|
|
156
154
|
"current_phase": "Current state (if applicable)",
|
|
157
|
-
"recent_achievements": "What was accomplished (if applicable)",
|
|
158
|
-
"active_challenges": "What remains (if applicable)"
|
|
155
|
+
"recent_achievements": ["What was accomplished (if applicable)"],
|
|
156
|
+
"active_challenges": ["What remains (if applicable)"],
|
|
157
|
+
"next_steps": ["Planned next actions (if applicable)"]
|
|
159
158
|
},
|
|
160
159
|
"memories": [
|
|
161
160
|
{
|
|
@@ -207,8 +206,7 @@ Return ONLY this JSON structure:
|
|
|
207
206
|
} : undefined,
|
|
208
207
|
memories: this._parseMemories(data.memories ?? []),
|
|
209
208
|
}
|
|
210
|
-
} catch
|
|
211
|
-
console.error('Failed to parse curation response:', error)
|
|
209
|
+
} catch {
|
|
212
210
|
return {
|
|
213
211
|
session_summary: '',
|
|
214
212
|
memories: [],
|
|
@@ -328,14 +326,6 @@ Return ONLY this JSON structure:
|
|
|
328
326
|
)
|
|
329
327
|
}
|
|
330
328
|
|
|
331
|
-
// Log the command we're about to execute
|
|
332
|
-
console.log(`\n📋 Executing CLI command:`)
|
|
333
|
-
console.log(` Command: ${this._config.cliCommand}`)
|
|
334
|
-
console.log(` Args: --resume ${sessionId} -p [user_message] --append-system-prompt [curation_instructions] --output-format json --max-turns 1`)
|
|
335
|
-
console.log(` CWD: ${cwd || 'not set'}`)
|
|
336
|
-
console.log(` User message: "${userMessage.slice(0, 50)}..."`)
|
|
337
|
-
console.log(` System prompt length: ${systemPrompt.length} chars`)
|
|
338
|
-
|
|
339
329
|
// Execute CLI
|
|
340
330
|
const proc = Bun.spawn([this._config.cliCommand, ...args], {
|
|
341
331
|
cwd,
|
|
@@ -353,24 +343,7 @@ Return ONLY this JSON structure:
|
|
|
353
343
|
])
|
|
354
344
|
const exitCode = await proc.exited
|
|
355
345
|
|
|
356
|
-
// Log the results
|
|
357
|
-
console.log(`\n📤 CLI Response:`)
|
|
358
|
-
console.log(` Exit code: ${exitCode}`)
|
|
359
|
-
console.log(` Stdout length: ${stdout.length} chars`)
|
|
360
|
-
console.log(` Stderr length: ${stderr.length} chars`)
|
|
361
|
-
|
|
362
|
-
if (stderr) {
|
|
363
|
-
console.log(`\n⚠️ Stderr output:`)
|
|
364
|
-
console.log(stderr.slice(0, 1000)) // First 1000 chars
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (stdout) {
|
|
368
|
-
console.log(`\n📥 Stdout output (first 500 chars):`)
|
|
369
|
-
console.log(stdout.slice(0, 500))
|
|
370
|
-
}
|
|
371
|
-
|
|
372
346
|
if (exitCode !== 0) {
|
|
373
|
-
console.error(`\n❌ CLI exited with code ${exitCode}`)
|
|
374
347
|
return { session_summary: '', memories: [] }
|
|
375
348
|
}
|
|
376
349
|
|
|
@@ -381,9 +354,6 @@ Return ONLY this JSON structure:
|
|
|
381
354
|
|
|
382
355
|
// Check for error response FIRST (like Python does)
|
|
383
356
|
if (cliOutput.type === 'error' || cliOutput.is_error === true) {
|
|
384
|
-
console.log(`\n❌ CLI returned error:`)
|
|
385
|
-
console.log(` Type: ${cliOutput.type}`)
|
|
386
|
-
console.log(` Message: ${cliOutput.message || cliOutput.error || 'Unknown error'}`)
|
|
387
357
|
return { session_summary: '', memories: [] }
|
|
388
358
|
}
|
|
389
359
|
|
|
@@ -391,10 +361,7 @@ Return ONLY this JSON structure:
|
|
|
391
361
|
let aiResponse = ''
|
|
392
362
|
if (typeof cliOutput.result === 'string') {
|
|
393
363
|
aiResponse = cliOutput.result
|
|
394
|
-
console.log(`\n📦 Extracted result from CLI wrapper (${aiResponse.length} chars)`)
|
|
395
364
|
} else {
|
|
396
|
-
console.log(`\n⚠️ No result field in CLI output`)
|
|
397
|
-
console.log(` Keys: ${Object.keys(cliOutput).join(', ')}`)
|
|
398
365
|
return { session_summary: '', memories: [] }
|
|
399
366
|
}
|
|
400
367
|
|
|
@@ -402,23 +369,15 @@ Return ONLY this JSON structure:
|
|
|
402
369
|
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
403
370
|
if (codeBlockMatch) {
|
|
404
371
|
aiResponse = codeBlockMatch[1]!.trim()
|
|
405
|
-
console.log(`📝 Extracted JSON from markdown code block`)
|
|
406
372
|
}
|
|
407
373
|
|
|
408
374
|
// Now find the JSON object (same regex as Python)
|
|
409
375
|
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0]
|
|
410
376
|
if (jsonMatch) {
|
|
411
|
-
|
|
412
|
-
const result = this.parseCurationResponse(jsonMatch)
|
|
413
|
-
console.log(` Parsed ${result.memories.length} memories`)
|
|
414
|
-
return result
|
|
415
|
-
} else {
|
|
416
|
-
console.log(`\n⚠️ No JSON object found in AI response`)
|
|
417
|
-
console.log(` Response preview: ${aiResponse.slice(0, 200)}...`)
|
|
377
|
+
return this.parseCurationResponse(jsonMatch)
|
|
418
378
|
}
|
|
419
|
-
} catch
|
|
420
|
-
|
|
421
|
-
console.log(` Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`)
|
|
379
|
+
} catch {
|
|
380
|
+
// Parse error - return empty result
|
|
422
381
|
}
|
|
423
382
|
|
|
424
383
|
return { session_summary: '', memories: [] }
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MEMORY ENGINE TESTS
|
|
3
|
+
// Basic integration tests for the memory system
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
7
|
+
import { MemoryEngine, createEngine } from './engine'
|
|
8
|
+
import { MemoryStore, createStore } from './store'
|
|
9
|
+
import { SmartVectorRetrieval, createRetrieval } from './retrieval'
|
|
10
|
+
import { join } from 'path'
|
|
11
|
+
import { rmSync, existsSync } from 'fs'
|
|
12
|
+
import type { CuratedMemory, StoredMemory } from '../types/memory'
|
|
13
|
+
|
|
14
|
+
const TEST_DIR = join(import.meta.dir, '../../test-data')
|
|
15
|
+
|
|
16
|
+
describe('MemoryStore', () => {
|
|
17
|
+
let store: MemoryStore
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
// Clean test directory
|
|
21
|
+
if (existsSync(TEST_DIR)) {
|
|
22
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
23
|
+
}
|
|
24
|
+
store = createStore({ basePath: TEST_DIR })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
store.close()
|
|
29
|
+
if (existsSync(TEST_DIR)) {
|
|
30
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should store and retrieve a memory', async () => {
|
|
35
|
+
const memory: CuratedMemory = {
|
|
36
|
+
content: 'Test memory content',
|
|
37
|
+
reasoning: 'This is important because...',
|
|
38
|
+
importance_weight: 0.8,
|
|
39
|
+
confidence_score: 0.9,
|
|
40
|
+
context_type: 'technical',
|
|
41
|
+
temporal_relevance: 'persistent',
|
|
42
|
+
knowledge_domain: 'testing',
|
|
43
|
+
emotional_resonance: 'neutral',
|
|
44
|
+
action_required: false,
|
|
45
|
+
problem_solution_pair: false,
|
|
46
|
+
semantic_tags: ['test', 'memory'],
|
|
47
|
+
trigger_phrases: ['testing memory'],
|
|
48
|
+
question_types: ['how to test'],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const id = await store.storeMemory('test-project', 'session-1', memory)
|
|
52
|
+
expect(id).toBeTruthy()
|
|
53
|
+
|
|
54
|
+
const allMemories = await store.getAllMemories('test-project')
|
|
55
|
+
expect(allMemories.length).toBe(1)
|
|
56
|
+
expect(allMemories[0].content).toBe('Test memory content')
|
|
57
|
+
expect(allMemories[0].importance_weight).toBe(0.8)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should track sessions', async () => {
|
|
61
|
+
const result1 = await store.getOrCreateSession('test-project', 'session-1')
|
|
62
|
+
expect(result1.isNew).toBe(true)
|
|
63
|
+
expect(result1.messageCount).toBe(0)
|
|
64
|
+
|
|
65
|
+
const result2 = await store.getOrCreateSession('test-project', 'session-1')
|
|
66
|
+
expect(result2.isNew).toBe(false)
|
|
67
|
+
|
|
68
|
+
const count = await store.incrementMessageCount('test-project', 'session-1')
|
|
69
|
+
expect(count).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('should store and retrieve session summaries', async () => {
|
|
73
|
+
await store.getOrCreateSession('test-project', 'session-1')
|
|
74
|
+
|
|
75
|
+
const id = await store.storeSessionSummary(
|
|
76
|
+
'test-project',
|
|
77
|
+
'session-1',
|
|
78
|
+
'We worked on the memory system together',
|
|
79
|
+
'collaborative'
|
|
80
|
+
)
|
|
81
|
+
expect(id).toBeTruthy()
|
|
82
|
+
|
|
83
|
+
const summary = await store.getLatestSummary('test-project')
|
|
84
|
+
expect(summary).toBeTruthy()
|
|
85
|
+
expect(summary!.summary).toBe('We worked on the memory system together')
|
|
86
|
+
expect(summary!.interaction_tone).toBe('collaborative')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('should return project stats', async () => {
|
|
90
|
+
await store.getOrCreateSession('test-project', 'session-1')
|
|
91
|
+
|
|
92
|
+
const memory: CuratedMemory = {
|
|
93
|
+
content: 'Test memory',
|
|
94
|
+
reasoning: 'Test',
|
|
95
|
+
importance_weight: 0.5,
|
|
96
|
+
confidence_score: 0.5,
|
|
97
|
+
context_type: 'general',
|
|
98
|
+
temporal_relevance: 'session',
|
|
99
|
+
knowledge_domain: 'test',
|
|
100
|
+
emotional_resonance: 'neutral',
|
|
101
|
+
action_required: false,
|
|
102
|
+
problem_solution_pair: false,
|
|
103
|
+
semantic_tags: [],
|
|
104
|
+
trigger_phrases: [],
|
|
105
|
+
question_types: [],
|
|
106
|
+
}
|
|
107
|
+
await store.storeMemory('test-project', 'session-1', memory)
|
|
108
|
+
|
|
109
|
+
const stats = await store.getProjectStats('test-project')
|
|
110
|
+
expect(stats.totalMemories).toBe(1)
|
|
111
|
+
expect(stats.totalSessions).toBe(1)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('SmartVectorRetrieval', () => {
|
|
116
|
+
const retrieval = createRetrieval()
|
|
117
|
+
|
|
118
|
+
const createTestMemory = (overrides: Partial<StoredMemory> = {}): StoredMemory => ({
|
|
119
|
+
id: 'test-' + Math.random().toString(36).slice(2),
|
|
120
|
+
content: 'Test memory',
|
|
121
|
+
reasoning: 'Test reasoning',
|
|
122
|
+
importance_weight: 0.5,
|
|
123
|
+
confidence_score: 0.5,
|
|
124
|
+
context_type: 'general',
|
|
125
|
+
temporal_relevance: 'persistent',
|
|
126
|
+
knowledge_domain: 'test',
|
|
127
|
+
emotional_resonance: 'neutral',
|
|
128
|
+
action_required: false,
|
|
129
|
+
problem_solution_pair: false,
|
|
130
|
+
semantic_tags: [],
|
|
131
|
+
trigger_phrases: [],
|
|
132
|
+
question_types: [],
|
|
133
|
+
session_id: 'session-1',
|
|
134
|
+
project_id: 'test-project',
|
|
135
|
+
created_at: Date.now(),
|
|
136
|
+
updated_at: Date.now(),
|
|
137
|
+
...overrides,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('should score action_required memories higher', () => {
|
|
141
|
+
const memories = [
|
|
142
|
+
createTestMemory({
|
|
143
|
+
id: 'normal',
|
|
144
|
+
content: 'Normal memory about tasks',
|
|
145
|
+
action_required: false,
|
|
146
|
+
importance_weight: 0.8,
|
|
147
|
+
trigger_phrases: ['todo', 'tasks'],
|
|
148
|
+
}),
|
|
149
|
+
createTestMemory({
|
|
150
|
+
id: 'action',
|
|
151
|
+
content: 'Action required: Fix the bug',
|
|
152
|
+
action_required: true,
|
|
153
|
+
importance_weight: 0.9,
|
|
154
|
+
trigger_phrases: ['todo', 'tasks', 'action', 'do'],
|
|
155
|
+
}),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
159
|
+
memories,
|
|
160
|
+
'What tasks do I need to do?',
|
|
161
|
+
new Float32Array(384),
|
|
162
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
163
|
+
5
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Action required should be prioritized (if any pass the gatekeeper)
|
|
167
|
+
// The action memory should score higher due to action boost
|
|
168
|
+
if (results.length > 0) {
|
|
169
|
+
const actionMemory = results.find(r => r.id === 'action')
|
|
170
|
+
expect(actionMemory).toBeTruthy()
|
|
171
|
+
} else {
|
|
172
|
+
// If no results, it means relevance threshold wasn't met - that's ok
|
|
173
|
+
expect(results.length).toBe(0)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('should match trigger phrases', () => {
|
|
178
|
+
const memories = [
|
|
179
|
+
createTestMemory({
|
|
180
|
+
id: 'with-trigger',
|
|
181
|
+
content: 'Memory about debugging',
|
|
182
|
+
trigger_phrases: ['debugging', 'bug fix'],
|
|
183
|
+
importance_weight: 0.7,
|
|
184
|
+
}),
|
|
185
|
+
createTestMemory({
|
|
186
|
+
id: 'no-trigger',
|
|
187
|
+
content: 'Unrelated memory',
|
|
188
|
+
trigger_phrases: ['cooking', 'recipes'],
|
|
189
|
+
importance_weight: 0.7,
|
|
190
|
+
}),
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
194
|
+
memories,
|
|
195
|
+
'I am debugging an issue',
|
|
196
|
+
new Float32Array(384),
|
|
197
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
198
|
+
5
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// The debugging memory should score higher
|
|
202
|
+
if (results.length > 0) {
|
|
203
|
+
const triggerMemory = results.find(r => r.id === 'with-trigger')
|
|
204
|
+
expect(triggerMemory).toBeTruthy()
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('should respect maxMemories limit', () => {
|
|
209
|
+
const memories = Array.from({ length: 10 }, (_, i) =>
|
|
210
|
+
createTestMemory({
|
|
211
|
+
id: `memory-${i}`,
|
|
212
|
+
content: `Memory ${i}`,
|
|
213
|
+
importance_weight: 0.8,
|
|
214
|
+
trigger_phrases: ['test'],
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const results = retrieval.retrieveRelevantMemories(
|
|
219
|
+
memories,
|
|
220
|
+
'test query',
|
|
221
|
+
new Float32Array(384),
|
|
222
|
+
{ session_id: 'test', project_id: 'test', message_count: 1 },
|
|
223
|
+
3 // Limit to 3
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
expect(results.length).toBeLessThanOrEqual(3)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('MemoryEngine', () => {
|
|
231
|
+
let engine: MemoryEngine
|
|
232
|
+
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
if (existsSync(TEST_DIR)) {
|
|
235
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
236
|
+
}
|
|
237
|
+
engine = createEngine({
|
|
238
|
+
centralPath: TEST_DIR,
|
|
239
|
+
maxMemories: 5,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
engine.close()
|
|
245
|
+
if (existsSync(TEST_DIR)) {
|
|
246
|
+
rmSync(TEST_DIR, { recursive: true })
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('should return primer on first message', async () => {
|
|
251
|
+
const result = await engine.getContext({
|
|
252
|
+
sessionId: 'session-1',
|
|
253
|
+
projectId: 'test-project',
|
|
254
|
+
currentMessage: 'Hello!',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(result.primer).toBeTruthy()
|
|
258
|
+
expect(result.memories.length).toBe(0)
|
|
259
|
+
expect(result.formatted).toContain('Continuing Session')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('should deduplicate memories within session', async () => {
|
|
263
|
+
// Store some memories first
|
|
264
|
+
const memory: CuratedMemory = {
|
|
265
|
+
content: 'Important memory about TypeScript',
|
|
266
|
+
reasoning: 'Technical insight',
|
|
267
|
+
importance_weight: 0.9,
|
|
268
|
+
confidence_score: 0.9,
|
|
269
|
+
context_type: 'technical',
|
|
270
|
+
temporal_relevance: 'persistent',
|
|
271
|
+
knowledge_domain: 'typescript',
|
|
272
|
+
emotional_resonance: 'discovery',
|
|
273
|
+
action_required: false,
|
|
274
|
+
problem_solution_pair: false,
|
|
275
|
+
semantic_tags: ['typescript', 'memory'],
|
|
276
|
+
trigger_phrases: ['typescript', 'ts'],
|
|
277
|
+
question_types: ['how to'],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await engine.storeCurationResult('test-project', 'session-0', {
|
|
281
|
+
session_summary: 'Previous session',
|
|
282
|
+
memories: [memory],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// First context request should get the memory
|
|
286
|
+
const result1 = await engine.getContext({
|
|
287
|
+
sessionId: 'session-1',
|
|
288
|
+
projectId: 'test-project',
|
|
289
|
+
currentMessage: 'First message',
|
|
290
|
+
})
|
|
291
|
+
// This is the primer (message count 0)
|
|
292
|
+
|
|
293
|
+
// Track a message
|
|
294
|
+
await engine.trackMessage('test-project', 'session-1')
|
|
295
|
+
|
|
296
|
+
// Second request with TypeScript query
|
|
297
|
+
const result2 = await engine.getContext({
|
|
298
|
+
sessionId: 'session-1',
|
|
299
|
+
projectId: 'test-project',
|
|
300
|
+
currentMessage: 'Tell me about TypeScript',
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Third request - same session, should NOT get same memory again
|
|
304
|
+
await engine.trackMessage('test-project', 'session-1')
|
|
305
|
+
const result3 = await engine.getContext({
|
|
306
|
+
sessionId: 'session-1',
|
|
307
|
+
projectId: 'test-project',
|
|
308
|
+
currentMessage: 'More about TypeScript',
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// The memory should only appear once per session
|
|
312
|
+
const memory2Count = result2.memories.length
|
|
313
|
+
const memory3Count = result3.memories.length
|
|
314
|
+
|
|
315
|
+
// Second query should find the memory, third should not (already injected)
|
|
316
|
+
console.log(`Result2 memories: ${memory2Count}, Result3 memories: ${memory3Count}`)
|
|
317
|
+
|
|
318
|
+
// At least verify the deduplication logic is running
|
|
319
|
+
expect(result3.memories.length).toBeLessThanOrEqual(result2.memories.length)
|
|
320
|
+
})
|
|
321
|
+
})
|