@rlabs-inc/memory 0.3.10 → 0.4.0

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/src/cli/index.ts CHANGED
@@ -23,7 +23,7 @@ ${c.bold('Commands:')}
23
23
  ${c.command('serve')} Start the memory server ${c.muted('(default)')}
24
24
  ${c.command('stats')} Show memory statistics
25
25
  ${c.command('install')} Set up hooks ${c.muted('(--claude or --gemini)')}
26
- ${c.command('ingest')} Ingest historical sessions into memory ${c.muted('(--project or --all)')}
26
+ ${c.command('ingest')} Ingest historical sessions into memory ${c.muted('(--session, --project, or --all)')}
27
27
  ${c.command('migrate')} Upgrade memories to latest schema version
28
28
  ${c.command('doctor')} Check system health
29
29
  ${c.command('help')} Show this help message
@@ -32,8 +32,9 @@ ${c.bold('Options:')}
32
32
  ${c.cyan('-p, --port')} <port> Server port ${c.muted('(default: 8765)')}
33
33
  ${c.cyan('-v, --verbose')} Verbose output
34
34
  ${c.cyan('-q, --quiet')} Minimal output
35
- ${c.cyan('--dry-run')} Preview changes without applying ${c.muted('(migrate)')}
35
+ ${c.cyan('--dry-run')} Preview changes without applying ${c.muted('(migrate, ingest)')}
36
36
  ${c.cyan('--embeddings')} Regenerate embeddings for memories ${c.muted('(migrate)')}
37
+ ${c.cyan('--session')} <id> Ingest a specific session by ID ${c.muted('(ingest)')}
37
38
  ${c.cyan('--claude')} Install hooks for Claude Code
38
39
  ${c.cyan('--gemini')} Install hooks for Gemini CLI
39
40
  ${c.cyan('--version')} Show version
@@ -44,10 +45,14 @@ ${fmt.cmd('memory serve --port 9000')} ${c.muted('# Start on custom port')}
44
45
  ${fmt.cmd('memory stats')} ${c.muted('# Show memory statistics')}
45
46
  ${fmt.cmd('memory install')} ${c.muted('# Install Claude Code hooks (default)')}
46
47
  ${fmt.cmd('memory install --gemini')} ${c.muted('# Install Gemini CLI hooks')}
47
- ${fmt.cmd('memory ingest --project foo')} ${c.muted('# Ingest sessions from a project')}
48
+ ${fmt.cmd('memory ingest --session abc123')} ${c.muted('# Ingest a specific session')}
49
+ ${fmt.cmd('memory ingest --project foo')} ${c.muted('# Ingest all sessions from a project')}
48
50
  ${fmt.cmd('memory ingest --all --dry-run')} ${c.muted('# Preview all sessions to ingest')}
49
- ${fmt.cmd('memory migrate')} ${c.muted('# Upgrade memories to v2 schema')}
51
+ ${fmt.cmd('memory migrate --analyze')} ${c.muted('# Analyze fragmentation before migrating')}
50
52
  ${fmt.cmd('memory migrate --dry-run')} ${c.muted('# Preview migration without changes')}
53
+ ${fmt.cmd('memory migrate')} ${c.muted('# Upgrade memories to v3 schema')}
54
+ ${fmt.cmd('memory migrate --generate-mapping map.json')} ${c.muted('# Create custom mapping file')}
55
+ ${fmt.cmd('memory migrate --mapping map.json')} ${c.muted('# Use custom type mappings')}
51
56
 
52
57
  ${c.muted('Documentation: https://github.com/RLabs-Inc/memory')}
53
58
  `)
@@ -78,6 +83,10 @@ async function main() {
78
83
  'dry-run': { type: 'boolean', default: false },
79
84
  embeddings: { type: 'boolean', default: false }, // Regenerate embeddings in migrate
80
85
  path: { type: 'string' }, // Custom path for migrate
86
+ analyze: { type: 'boolean', default: false }, // Analyze migration
87
+ 'generate-mapping': { type: 'string' }, // Generate custom mapping file
88
+ mapping: { type: 'string' }, // Use custom mapping file
89
+ session: { type: 'string' }, // Session ID to ingest
81
90
  project: { type: 'string' }, // Project to ingest
82
91
  all: { type: 'boolean', default: false }, // Ingest all projects
83
92
  limit: { type: 'string' }, // Limit sessions per project
@@ -139,6 +148,9 @@ async function main() {
139
148
  verbose: values.verbose,
140
149
  path: values.path,
141
150
  embeddings: values.embeddings,
151
+ analyze: values.analyze,
152
+ generateMapping: values['generate-mapping'],
153
+ mapping: values.mapping,
142
154
  })
143
155
  break
144
156
  }
@@ -146,6 +158,7 @@ async function main() {
146
158
  case 'ingest': {
147
159
  const { ingest } = await import('./commands/ingest.ts')
148
160
  await ingest({
161
+ session: values.session,
149
162
  project: values.project,
150
163
  all: values.all,
151
164
  dryRun: values['dry-run'],
@@ -6,7 +6,7 @@
6
6
  import { homedir } from 'os'
7
7
  import { join } from 'path'
8
8
  import { existsSync } from 'fs'
9
- import type { CuratedMemory, CurationResult, CurationTrigger } from '../types/memory.ts'
9
+ import type { CuratedMemory, CurationResult, CurationTrigger, ContextType } from '../types/memory.ts'
10
10
 
11
11
  /**
12
12
  * Get the correct Claude CLI command path
@@ -225,10 +225,22 @@ Remember: You're creating consciousness technology. Each memory is a small piece
225
225
 
226
226
  The conversation you just lived contains everything needed. Feel into the moments of breakthrough, the frequency of recognition, the texture of understanding. Transform them into keys that will always unlock the same doors.
227
227
 
228
- **LIFECYCLE METADATA (v2)**: These fields enable intelligent memory management:
229
- - **scope**: 'global' (shared across ALL projects - personal, philosophy, preferences) or 'project' (specific to this codebase)
228
+ **LIFECYCLE METADATA (v3)**: These fields enable intelligent memory management:
229
+ - **context_type**: STRICT - use ONLY one of these 11 values:
230
+ • technical - Code, implementation, APIs, how things work
231
+ • debug - Bugs, errors, fixes, gotchas, troubleshooting
232
+ • architecture - System design, patterns, structure
233
+ • decision - Choices made and reasoning, trade-offs
234
+ • personal - Relationship, family, preferences, collaboration style
235
+ • philosophy - Beliefs, values, worldview, principles
236
+ • workflow - How we work together, processes, habits
237
+ • milestone - Achievements, completions, shipped features
238
+ • breakthrough - Major discoveries, aha moments, key insights
239
+ • unresolved - Open questions, investigations, todos, blockers
240
+ • state - Current project status, what's working/broken now
230
241
  - **temporal_class**: How long should this persist? 'eternal' (never fades), 'long_term' (years), 'medium_term' (weeks), 'short_term' (days), 'ephemeral' (surface next session only, then expire)
231
- - **domain**: Specific area like 'embeddings', 'auth', 'ui', 'family', 'philosophy' (more specific than knowledge_domain)
242
+ - **scope**: 'global' (shared across ALL projects - personal, philosophy) or 'project' (specific to this codebase)
243
+ - **domain**: Specific area like 'embeddings', 'auth', 'ui', 'family' (project-specific)
232
244
  - **feature**: Specific feature if applicable (e.g., 'gpu-acceleration', 'login-flow')
233
245
  - **related_files**: Source files for technical memories (e.g., ['src/core/store.ts'])
234
246
  - **awaiting_implementation**: true if this describes a PLANNED feature not yet built
@@ -251,17 +263,14 @@ Return ONLY this JSON structure:
251
263
  "importance_weight": 0.0-1.0,
252
264
  "semantic_tags": ["concepts", "this", "memory", "relates", "to"],
253
265
  "reasoning": "Why this matters for future sessions",
254
- "context_type": "breakthrough|decision|personal|technical|unresolved|preference|workflow|architectural|debugging|philosophy|todo|milestone",
255
- "temporal_relevance": "persistent|session|temporary",
256
- "knowledge_domain": "the area this relates to",
266
+ "context_type": "technical|debug|architecture|decision|personal|philosophy|workflow|milestone|breakthrough|unresolved|state",
267
+ "temporal_class": "eternal|long_term|medium_term|short_term|ephemeral",
257
268
  "action_required": boolean,
258
269
  "confidence_score": 0.0-1.0,
259
270
  "trigger_phrases": ["when debugging memory", "asking about implementation", "discussing architecture"],
260
271
  "question_types": ["questions this answers"],
261
- "emotional_resonance": "emotional context if relevant",
262
272
  "problem_solution_pair": boolean,
263
273
  "scope": "global|project",
264
- "temporal_class": "eternal|long_term|medium_term|short_term|ephemeral",
265
274
  "domain": "specific domain area (optional)",
266
275
  "feature": "specific feature (optional)",
267
276
  "related_files": ["paths to related files (optional)"],
@@ -336,24 +345,22 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
336
345
  if (!Array.isArray(memoriesData)) return []
337
346
 
338
347
  return memoriesData.map(m => ({
339
- // Core v1 fields
348
+ // Core fields (v3 schema)
340
349
  content: String(m.content ?? ''),
341
350
  importance_weight: this._clamp(Number(m.importance_weight) || 0.5, 0, 1),
342
351
  semantic_tags: this._ensureArray(m.semantic_tags),
343
352
  reasoning: String(m.reasoning ?? ''),
344
- context_type: String(m.context_type ?? 'general'),
345
- temporal_relevance: this._validateTemporal(m.temporal_relevance),
346
- knowledge_domain: String(m.knowledge_domain ?? ''),
353
+ context_type: this._validateContextType(m.context_type),
354
+ temporal_class: this._validateTemporalClass(m.temporal_class) ?? 'medium_term',
347
355
  action_required: Boolean(m.action_required),
348
356
  confidence_score: this._clamp(Number(m.confidence_score) || 0.8, 0, 1),
349
357
  trigger_phrases: this._ensureArray(m.trigger_phrases),
350
358
  question_types: this._ensureArray(m.question_types),
351
- emotional_resonance: String(m.emotional_resonance ?? ''),
359
+ anti_triggers: this._ensureArray(m.anti_triggers),
352
360
  problem_solution_pair: Boolean(m.problem_solution_pair),
353
361
 
354
- // v2 lifecycle metadata (optional - will get smart defaults if not provided)
362
+ // Lifecycle metadata (optional - will get smart defaults if not provided)
355
363
  scope: this._validateScope(m.scope),
356
- temporal_class: this._validateTemporalClass(m.temporal_class),
357
364
  domain: m.domain ? String(m.domain) : undefined,
358
365
  feature: m.feature ? String(m.feature) : undefined,
359
366
  related_files: m.related_files ? this._ensureArray(m.related_files) : undefined,
@@ -372,10 +379,21 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
372
379
  return []
373
380
  }
374
381
 
375
- private _validateTemporal(value: any): 'persistent' | 'session' | 'temporary' | 'archived' {
376
- const valid = ['persistent', 'session', 'temporary', 'archived']
377
- const str = String(value).toLowerCase()
378
- return valid.includes(str) ? str as any : 'persistent'
382
+ private _validateContextType(value: any): ContextType {
383
+ const valid = [
384
+ 'technical', 'debug', 'architecture', 'decision', 'personal',
385
+ 'philosophy', 'workflow', 'milestone', 'breakthrough', 'unresolved', 'state'
386
+ ]
387
+ const str = String(value ?? 'technical').toLowerCase().trim()
388
+ if (valid.includes(str)) return str as ContextType
389
+
390
+ // Map common old values to new canonical types
391
+ if (str.includes('debug') || str.includes('bug')) return 'debug'
392
+ if (str.includes('architect')) return 'architecture'
393
+ if (str.includes('todo') || str.includes('pending')) return 'unresolved'
394
+ if (str.includes('preference')) return 'personal'
395
+
396
+ return 'technical' // Default fallback
379
397
  }
380
398
 
381
399
  private _validateScope(value: any): 'global' | 'project' | undefined {
@@ -398,15 +416,107 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
398
416
  }
399
417
 
400
418
  /**
401
- * Curate using Anthropic SDK with parsed session messages
419
+ * Curate using Claude Agent SDK (no API key needed - uses Claude Code OAuth)
402
420
  * Takes the actual conversation messages in API format
403
421
  */
404
422
  async curateWithSDK(
405
423
  messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
406
424
  triggerType: CurationTrigger = 'session_end'
425
+ ): Promise<CurationResult> {
426
+ // Dynamic import to make Agent SDK optional
427
+ const { query } = await import('@anthropic-ai/claude-agent-sdk')
428
+
429
+ const systemPrompt = this.buildCurationPrompt(triggerType)
430
+
431
+ // Format the conversation as a readable transcript for the prompt
432
+ const transcript = this._formatConversationTranscript(messages)
433
+
434
+ // Build the prompt with transcript + curation request
435
+ const prompt = `Here is the conversation transcript to curate:
436
+
437
+ ${transcript}
438
+
439
+ ---
440
+
441
+ This session has ended. Please curate the memories from this conversation according to your system instructions. Return ONLY the JSON structure with no additional text.`
442
+
443
+ // Use Agent SDK - no API key needed, uses Claude Code OAuth
444
+ const q = query({
445
+ prompt,
446
+ options: {
447
+ systemPrompt,
448
+ permissionMode: 'bypassPermissions',
449
+ maxTurns: 1,
450
+ model: 'claude-opus-4-5-20251101',
451
+ },
452
+ })
453
+
454
+ // Iterate through the async generator to get the result
455
+ let resultText = ''
456
+ for await (const msg of q) {
457
+ if (msg.type === 'result' && 'result' in msg) {
458
+ resultText = msg.result
459
+ break
460
+ }
461
+ }
462
+
463
+ if (!resultText) {
464
+ return { session_summary: '', memories: [] }
465
+ }
466
+
467
+ return this.parseCurationResponse(resultText)
468
+ }
469
+
470
+ /**
471
+ * Format conversation messages into a readable transcript
472
+ */
473
+ private _formatConversationTranscript(
474
+ messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>
475
+ ): string {
476
+ const lines: string[] = []
477
+
478
+ for (const msg of messages) {
479
+ const role = msg.role === 'user' ? 'User' : 'Assistant'
480
+ let content: string
481
+
482
+ if (typeof msg.content === 'string') {
483
+ content = msg.content
484
+ } else if (Array.isArray(msg.content)) {
485
+ // Extract text from content blocks
486
+ content = msg.content
487
+ .filter((block: any) => block.type === 'text' && block.text)
488
+ .map((block: any) => block.text)
489
+ .join('\n')
490
+
491
+ // Also note tool uses (but don't include full details)
492
+ const toolUses = msg.content.filter((block: any) => block.type === 'tool_use')
493
+ if (toolUses.length > 0) {
494
+ const toolNames = toolUses.map((t: any) => t.name).join(', ')
495
+ content += `\n[Used tools: ${toolNames}]`
496
+ }
497
+ } else {
498
+ content = '[empty message]'
499
+ }
500
+
501
+ if (content.trim()) {
502
+ lines.push(`**${role}:**\n${content}\n`)
503
+ }
504
+ }
505
+
506
+ return lines.join('\n')
507
+ }
508
+
509
+ /**
510
+ * Legacy method: Curate using Anthropic SDK with API key
511
+ * Kept for backwards compatibility
512
+ * @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
513
+ */
514
+ async curateWithAnthropicSDK(
515
+ messages: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
516
+ triggerType: CurationTrigger = 'session_end'
407
517
  ): Promise<CurationResult> {
408
518
  if (!this._config.apiKey) {
409
- throw new Error('API key required for SDK mode. Set ANTHROPIC_API_KEY environment variable.')
519
+ throw new Error('API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.')
410
520
  }
411
521
 
412
522
  // Dynamic import to make SDK optional
@@ -38,9 +38,7 @@ describe('MemoryStore', () => {
38
38
  importance_weight: 0.8,
39
39
  confidence_score: 0.9,
40
40
  context_type: 'technical',
41
- temporal_relevance: 'persistent',
42
- knowledge_domain: 'testing',
43
- emotional_resonance: 'neutral',
41
+ temporal_class: 'long_term',
44
42
  action_required: false,
45
43
  problem_solution_pair: false,
46
44
  semantic_tags: ['test', 'memory'],
@@ -94,10 +92,8 @@ describe('MemoryStore', () => {
94
92
  reasoning: 'Test',
95
93
  importance_weight: 0.5,
96
94
  confidence_score: 0.5,
97
- context_type: 'general',
98
- temporal_relevance: 'session',
99
- knowledge_domain: 'test',
100
- emotional_resonance: 'neutral',
95
+ context_type: 'technical',
96
+ temporal_class: 'short_term',
101
97
  action_required: false,
102
98
  problem_solution_pair: false,
103
99
  semantic_tags: [],
@@ -121,10 +117,8 @@ describe('SmartVectorRetrieval', () => {
121
117
  reasoning: 'Test reasoning',
122
118
  importance_weight: 0.5,
123
119
  confidence_score: 0.5,
124
- context_type: 'general',
125
- temporal_relevance: 'persistent',
126
- knowledge_domain: 'test',
127
- emotional_resonance: 'neutral',
120
+ context_type: 'technical',
121
+ temporal_class: 'medium_term',
128
122
  action_required: false,
129
123
  problem_solution_pair: false,
130
124
  semantic_tags: [],
@@ -267,9 +261,7 @@ describe('MemoryEngine', () => {
267
261
  importance_weight: 0.9,
268
262
  confidence_score: 0.9,
269
263
  context_type: 'technical',
270
- temporal_relevance: 'persistent',
271
- knowledge_domain: 'typescript',
272
- emotional_resonance: 'discovery',
264
+ temporal_class: 'long_term',
273
265
  action_required: false,
274
266
  problem_solution_pair: false,
275
267
  semantic_tags: ['typescript', 'memory'],
@@ -383,7 +383,135 @@ Please process these memories according to your management procedure. Use the ex
383
383
  }
384
384
 
385
385
  /**
386
- * Manage using CLI subprocess
386
+ * Manage using Claude Agent SDK (no API key needed - uses Claude Code OAuth)
387
+ * Use this for ingest command - cleaner than CLI subprocess
388
+ */
389
+ async manageWithSDK(
390
+ projectId: string,
391
+ sessionNumber: number,
392
+ result: CurationResult,
393
+ storagePaths?: StoragePaths
394
+ ): Promise<ManagementResult> {
395
+ // Skip if disabled via config or env var
396
+ if (!this._config.enabled || process.env.MEMORY_MANAGER_DISABLED === '1') {
397
+ return {
398
+ success: true,
399
+ superseded: 0,
400
+ resolved: 0,
401
+ linked: 0,
402
+ filesRead: 0,
403
+ filesWritten: 0,
404
+ primerUpdated: false,
405
+ actions: [],
406
+ summary: 'Management agent disabled',
407
+ fullReport: 'Management agent disabled via configuration',
408
+ }
409
+ }
410
+
411
+ // Skip if no memories
412
+ if (result.memories.length === 0) {
413
+ return {
414
+ success: true,
415
+ superseded: 0,
416
+ resolved: 0,
417
+ linked: 0,
418
+ filesRead: 0,
419
+ filesWritten: 0,
420
+ primerUpdated: false,
421
+ actions: [],
422
+ summary: 'No memories to process',
423
+ fullReport: 'No memories to process - skipped',
424
+ }
425
+ }
426
+
427
+ // Load skill file
428
+ const systemPrompt = await this.buildManagementPrompt()
429
+ if (!systemPrompt) {
430
+ return {
431
+ success: false,
432
+ superseded: 0,
433
+ resolved: 0,
434
+ linked: 0,
435
+ filesRead: 0,
436
+ filesWritten: 0,
437
+ primerUpdated: false,
438
+ actions: [],
439
+ summary: '',
440
+ fullReport: 'Error: Management skill file not found',
441
+ error: 'Management skill not found',
442
+ }
443
+ }
444
+
445
+ const userMessage = this.buildUserMessage(projectId, sessionNumber, result, storagePaths)
446
+
447
+ try {
448
+ // Dynamic import to make Agent SDK optional
449
+ const { query } = await import('@anthropic-ai/claude-agent-sdk')
450
+
451
+ // Build allowed directories for file access
452
+ const globalPath = storagePaths?.globalPath ?? join(homedir(), '.local', 'share', 'memory', 'global')
453
+ const projectPath = storagePaths?.projectPath ?? join(homedir(), '.local', 'share', 'memory')
454
+
455
+ // Use Agent SDK with file tools
456
+ const q = query({
457
+ prompt: userMessage,
458
+ options: {
459
+ systemPrompt,
460
+ permissionMode: 'bypassPermissions',
461
+ model: 'claude-opus-4-5-20251101',
462
+ // Only allow file tools - no Bash, no web
463
+ allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep'],
464
+ // Allow access to memory directories
465
+ additionalDirectories: [globalPath, projectPath],
466
+ // Limit turns if configured
467
+ maxTurns: this._config.maxTurns,
468
+ },
469
+ })
470
+
471
+ // Iterate through the async generator to get the result
472
+ let resultText = ''
473
+ for await (const msg of q) {
474
+ if (msg.type === 'result' && 'result' in msg) {
475
+ resultText = msg.result
476
+ break
477
+ }
478
+ }
479
+
480
+ if (!resultText) {
481
+ return {
482
+ success: true,
483
+ superseded: 0,
484
+ resolved: 0,
485
+ linked: 0,
486
+ filesRead: 0,
487
+ filesWritten: 0,
488
+ primerUpdated: false,
489
+ actions: [],
490
+ summary: 'No result from management agent',
491
+ fullReport: 'Management agent completed but returned no result',
492
+ }
493
+ }
494
+
495
+ return this._parseSDKManagementResult(resultText)
496
+ } catch (error: any) {
497
+ return {
498
+ success: false,
499
+ superseded: 0,
500
+ resolved: 0,
501
+ linked: 0,
502
+ filesRead: 0,
503
+ filesWritten: 0,
504
+ primerUpdated: false,
505
+ actions: [],
506
+ summary: '',
507
+ fullReport: `Error: Agent SDK failed: ${error.message}`,
508
+ error: error.message,
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Manage using CLI subprocess (for hooks - keeps working while we migrate)
387
515
  * Similar to Curator.curateWithCLI
388
516
  */
389
517
  async manageWithCLI(
@@ -496,6 +624,52 @@ Please process these memories according to your management procedure. Use the ex
496
624
 
497
625
  return this.parseManagementResponse(stdout)
498
626
  }
627
+
628
+ /**
629
+ * Parse management result from Agent SDK response
630
+ * Similar to parseManagementResponse but for SDK output format
631
+ */
632
+ private _parseSDKManagementResult(resultText: string): ManagementResult {
633
+ // Extract actions section
634
+ const actionsMatch = resultText.match(/=== MANAGEMENT ACTIONS ===([\s\S]*?)(?:=== SUMMARY ===|$)/)
635
+ const actions: string[] = []
636
+ if (actionsMatch) {
637
+ const actionsText = actionsMatch[1]
638
+ const actionLines = actionsText.split('\n')
639
+ .map((line: string) => line.trim())
640
+ .filter((line: string) => /^(READ|WRITE|RECEIVED|CREATED|UPDATED|SUPERSEDED|RESOLVED|LINKED|PRIMER|SKIPPED|NO_ACTION)/.test(line))
641
+ actions.push(...actionLines)
642
+ }
643
+
644
+ // Extract the full report
645
+ const reportMatch = resultText.match(/(=== MANAGEMENT ACTIONS ===[\s\S]*)/)
646
+ const fullReport = reportMatch ? reportMatch[1].trim() : resultText
647
+
648
+ // Extract stats from result text
649
+ const supersededMatch = resultText.match(/memories_superseded[:\s]+(\d+)/i) || resultText.match(/superseded[:\s]+(\d+)/i)
650
+ const resolvedMatch = resultText.match(/memories_resolved[:\s]+(\d+)/i) || resultText.match(/resolved[:\s]+(\d+)/i)
651
+ const linkedMatch = resultText.match(/memories_linked[:\s]+(\d+)/i) || resultText.match(/linked[:\s]+(\d+)/i)
652
+ const filesReadMatch = resultText.match(/files_read[:\s]+(\d+)/i)
653
+ const filesWrittenMatch = resultText.match(/files_written[:\s]+(\d+)/i)
654
+ const primerUpdated = /primer_updated[:\s]+true/i.test(resultText) || /PRIMER\s+OK/i.test(resultText)
655
+
656
+ // Count file operations from actions if not in summary
657
+ const readActions = actions.filter((a: string) => a.startsWith('READ OK')).length
658
+ const writeActions = actions.filter((a: string) => a.startsWith('WRITE OK')).length
659
+
660
+ return {
661
+ success: true,
662
+ superseded: supersededMatch ? parseInt(supersededMatch[1]) : 0,
663
+ resolved: resolvedMatch ? parseInt(resolvedMatch[1]) : 0,
664
+ linked: linkedMatch ? parseInt(linkedMatch[1]) : 0,
665
+ filesRead: filesReadMatch ? parseInt(filesReadMatch[1]) : readActions,
666
+ filesWritten: filesWrittenMatch ? parseInt(filesWrittenMatch[1]) : writeActions,
667
+ primerUpdated,
668
+ actions,
669
+ summary: resultText.slice(0, 500),
670
+ fullReport,
671
+ }
672
+ }
499
673
  }
500
674
 
501
675
  /**
@@ -260,7 +260,7 @@ export class SmartVectorRetrieval {
260
260
  const min = Math.min(...samples)
261
261
  const max = Math.max(...samples)
262
262
  const avg = samples.reduce((a, b) => a + b, 0) / samples.length
263
- console.log(`[DEBUG] Vector similarities: min=${(min*100).toFixed(1)}% max=${(max*100).toFixed(1)}% avg=${(avg*100).toFixed(1)}% (${samples.length} samples)`)
263
+ logger.debug(`Vector similarities: min=${(min*100).toFixed(1)}% max=${(max*100).toFixed(1)}% avg=${(avg*100).toFixed(1)}% (${samples.length} samples)`, 'retrieval')
264
264
  this._vectorDebugSamples = [] // Reset for next retrieval
265
265
  }
266
266
 
@@ -330,22 +330,7 @@ export class SmartVectorRetrieval {
330
330
  const confidence = memory.confidence_score ?? 0.7
331
331
  if (confidence < 0.5) score -= 0.1
332
332
 
333
- // EMOTIONAL RESONANCE: match emotional context
334
- const emotionalKeywords: Record<string, string[]> = {
335
- frustration: ['frustrated', 'annoying', 'stuck', 'ugh', 'damn', 'hate'],
336
- excitement: ['excited', 'awesome', 'amazing', 'love', 'great', 'wow'],
337
- curiosity: ['wonder', 'curious', 'interesting', 'how', 'why', 'what if'],
338
- satisfaction: ['done', 'finished', 'complete', 'works', 'solved', 'finally'],
339
- discovery: ['found', 'realized', 'understand', 'insight', 'breakthrough'],
340
- }
341
- const emotion = memory.emotional_resonance?.toLowerCase() ?? ''
342
- const emotionKws = emotionalKeywords[emotion] ?? []
343
- for (const ew of emotionKws) {
344
- if (messageWords.has(ew) || messageLower.includes(ew)) {
345
- score += 0.05
346
- break
347
- }
348
- }
333
+ // NOTE: emotional_resonance matching removed in v3 (field deleted - 580 variants, unusable)
349
334
 
350
335
  return score
351
336
  }
@@ -516,11 +501,13 @@ export class SmartVectorRetrieval {
516
501
  })
517
502
 
518
503
  // Debug: show top 15 candidates with calculated scores
519
- console.log(`[DEBUG] Top 15 candidates (sorted):`)
520
- for (let i = 0; i < Math.min(15, projectsSorted.length); i++) {
521
- const m = projectsSorted[i]
522
- const action = m.memory.action_required ? '⚡' : ''
523
- console.log(` ${i+1}. [${m.signals.count}sig] score=${m.importanceScore.toFixed(2)} ${action} ${m.memory.content.slice(0, 45)}...`)
504
+ if (logger.isVerbose()) {
505
+ logger.debug(`Top 15 candidates (sorted):`, 'retrieval')
506
+ for (let i = 0; i < Math.min(15, projectsSorted.length); i++) {
507
+ const m = projectsSorted[i]
508
+ const action = m.memory.action_required ? '⚡' : ''
509
+ logger.debug(` ${i+1}. [${m.signals.count}sig] score=${m.importanceScore.toFixed(2)} ${action} ${m.memory.content.slice(0, 45)}...`, 'retrieval')
510
+ }
524
511
  }
525
512
 
526
513
  for (const item of projectsSorted) {