@rlabs-inc/memory 0.4.13 → 0.4.15

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/README.md CHANGED
@@ -46,6 +46,21 @@ That's it. Now use Claude Code normally—memories are extracted and surfaced au
46
46
 
47
47
  ## Features
48
48
 
49
+ ### Action Items Signal (`***`)
50
+ Add `***` at the end of any message to retrieve all pending action items:
51
+
52
+ ```
53
+ "hey Watson, let's continue with the project ***"
54
+ ```
55
+
56
+ This returns all memories marked as:
57
+ - `action_required: true`
58
+ - `awaiting_implementation: true`
59
+ - `awaiting_decision: true`
60
+ - `context_type: 'unresolved'`
61
+
62
+ Zero overhead for normal messages—detection is a simple `endsWith` check.
63
+
49
64
  ### Semantic Embeddings
50
65
  Uses `all-MiniLM-L6-v2` for 384-dimensional embeddings. Memories are retrieved by meaning, not just keywords.
51
66
 
@@ -383,6 +398,21 @@ This isn't just about remembering facts. It's about preserving:
383
398
 
384
399
  ## Changelog
385
400
 
401
+ ### v0.4.15
402
+ - **Feature**: PATCH `/memory/:id` endpoint for updating memory metadata
403
+ - Supports: `importance_weight`, `exclude_from_retrieval`, `status`, `action_required`, and more
404
+ - Enables dashboards and tools for memory curation (promote/demote/bury)
405
+
406
+ ### v0.4.14
407
+ - **Feature**: Action items signal (`***`) - add to end of message to retrieve all pending items
408
+ - **Feature**: New `getActionItems()` retrieval function with special formatting
409
+ - Zero overhead for normal messages - detection is a simple string check
410
+
411
+ ### v0.4.13
412
+ - **Fix**: Segmented transcript curation for large sessions (400+ messages)
413
+ - **Improvement**: Unified curator fallback and ingest command behavior
414
+ - **Tech**: Segments at ~150k tokens, accumulates memories, merges summaries with part markers
415
+
386
416
  ### v0.4.12
387
417
  - **Simplify**: Removed Zod structured outputs - session resumption only
388
418
  - **Improvement**: Uses existing battle-tested JSON parser instead of Zod
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,7 @@
6
6
  import { homedir } from 'os'
7
7
  import { join } from 'path'
8
8
  import { MemoryStore, createStore } from './store.ts'
9
- import { SmartVectorRetrieval, createRetrieval, type SessionContext } from './retrieval.ts'
9
+ import { SmartVectorRetrieval, createRetrieval, getActionItems, type SessionContext } from './retrieval.ts'
10
10
  import type {
11
11
  CuratedMemory,
12
12
  StoredMemory,
@@ -66,6 +66,13 @@ export interface EngineConfig {
66
66
  personalMemoriesEnabled?: boolean
67
67
  }
68
68
 
69
+ /**
70
+ * Retrieval mode
71
+ * - 'normal': Standard activation signal retrieval (default)
72
+ * - 'action_items': Return all memories marked as requiring action
73
+ */
74
+ export type RetrievalMode = 'normal' | 'action_items'
75
+
69
76
  /**
70
77
  * Context request parameters
71
78
  */
@@ -75,6 +82,7 @@ export interface ContextRequest {
75
82
  currentMessage: string
76
83
  maxMemories?: number
77
84
  projectPath?: string // Required for 'local' storage mode
85
+ mode?: RetrievalMode // Retrieval mode (default: 'normal')
78
86
  }
79
87
 
80
88
  /**
@@ -212,7 +220,23 @@ export class MemoryEngine {
212
220
  return { memories: [], formatted: '' }
213
221
  }
214
222
 
215
- // Filter out already-injected memories (deduplication)
223
+ // ACTION ITEMS MODE: Return all memories marked as requiring action
224
+ // Triggered by *** signal at end of message
225
+ if (request.mode === 'action_items') {
226
+ const actionItems = getActionItems(allMemories, projectId)
227
+
228
+ // Update injected memories for deduplication
229
+ for (const memory of actionItems) {
230
+ injectedIds.add(memory.id)
231
+ }
232
+
233
+ return {
234
+ memories: actionItems,
235
+ formatted: this._formatActionItems(actionItems),
236
+ }
237
+ }
238
+
239
+ // NORMAL MODE: Filter out already-injected memories (deduplication)
216
240
  const candidateMemories = allMemories.filter(m => !injectedIds.has(m.id))
217
241
 
218
242
  if (!candidateMemories.length) {
@@ -380,6 +404,42 @@ export class MemoryEngine {
380
404
  return [...projectMemories, ...globalMemories]
381
405
  }
382
406
 
407
+ /**
408
+ * Update a memory's metadata
409
+ * Used for curation actions: promote/demote, bury, archive
410
+ */
411
+ async updateMemory(
412
+ projectId: string,
413
+ memoryId: string,
414
+ updates: {
415
+ importance_weight?: number
416
+ confidence_score?: number
417
+ exclude_from_retrieval?: boolean
418
+ status?: 'active' | 'pending' | 'superseded' | 'deprecated' | 'archived'
419
+ action_required?: boolean
420
+ awaiting_implementation?: boolean
421
+ awaiting_decision?: boolean
422
+ semantic_tags?: string[]
423
+ trigger_phrases?: string[]
424
+ },
425
+ projectPath?: string
426
+ ): Promise<{ success: boolean; updated_fields: string[] }> {
427
+ const store = await this._getStore(projectId, projectPath)
428
+ return store.updateMemory(projectId, memoryId, updates)
429
+ }
430
+
431
+ /**
432
+ * Get a single memory by ID
433
+ */
434
+ async getMemory(
435
+ projectId: string,
436
+ memoryId: string,
437
+ projectPath?: string
438
+ ): Promise<StoredMemory | null> {
439
+ const store = await this._getStore(projectId, projectPath)
440
+ return store.getMemory(projectId, memoryId)
441
+ }
442
+
383
443
  // ================================================================
384
444
  // FORMATTING
385
445
  // ================================================================
@@ -628,6 +688,56 @@ export class MemoryEngine {
628
688
  return parts.join('\n')
629
689
  }
630
690
 
691
+ /**
692
+ * Format action items for injection
693
+ * Different header to make it clear this is the full action items list
694
+ */
695
+ private _formatActionItems(memories: RetrievalResult[]): string {
696
+ if (!memories.length) {
697
+ return '# Action Items\n\nNo pending action items found.'
698
+ }
699
+
700
+ const parts: string[] = ['# Action Items']
701
+ parts.push(`\n*${memories.length} pending item${memories.length === 1 ? '' : 's'}*\n`)
702
+
703
+ for (const memory of memories) {
704
+ const importance = memory.importance_weight?.toFixed(1) || '0.5'
705
+ const emoji = getMemoryEmoji(memory.context_type || 'general')
706
+ const age = memory.updated_at ? this._formatAge(memory.updated_at) :
707
+ memory.created_at ? this._formatAge(memory.created_at) : ''
708
+
709
+ // Flags
710
+ const flags: string[] = []
711
+ if (memory.action_required) flags.push('⚡ACTION')
712
+ if (memory.awaiting_implementation) flags.push('🔨IMPL')
713
+ if (memory.awaiting_decision) flags.push('❓DECISION')
714
+ if (memory.context_type === 'unresolved') flags.push('❓UNRESOLVED')
715
+ const flagStr = flags.length ? ` [${flags.join(' ')}]` : ''
716
+
717
+ // Short ID for reference
718
+ const shortId = memory.id.slice(-6)
719
+
720
+ // Display: headline if available, otherwise content
721
+ const displayText = memory.headline || memory.content
722
+
723
+ parts.push(`[${emoji} ${importance} • ${age} • #${shortId}]${flagStr}`)
724
+ parts.push(`${displayText}`)
725
+
726
+ // Always show full content for action items (they need context)
727
+ if (memory.headline && memory.content) {
728
+ const contentLines = memory.content.split('\n')
729
+ for (const line of contentLines) {
730
+ if (line.trim()) {
731
+ parts.push(` ${line}`)
732
+ }
733
+ }
734
+ }
735
+ parts.push('') // Blank line between items
736
+ }
737
+
738
+ return parts.join('\n')
739
+ }
740
+
631
741
  /**
632
742
  * Get resolved storage paths for a project
633
743
  * Returns the actual paths based on current engine configuration
@@ -777,3 +777,65 @@ export class SmartVectorRetrieval {
777
777
  export function createRetrieval(): SmartVectorRetrieval {
778
778
  return new SmartVectorRetrieval()
779
779
  }
780
+
781
+ // ============================================================================
782
+ // ACTION ITEMS RETRIEVAL
783
+ // Returns all memories marked as requiring action
784
+ // Triggered by *** signal at end of message (detected in hook)
785
+ // ============================================================================
786
+
787
+ /**
788
+ * Get all memories marked as requiring action
789
+ *
790
+ * Filters for memories with:
791
+ * - action_required: true
792
+ * - awaiting_implementation: true
793
+ * - awaiting_decision: true
794
+ * - context_type: 'todo'
795
+ *
796
+ * Returns them sorted by importance, newest first within same importance
797
+ */
798
+ export function getActionItems(
799
+ allMemories: StoredMemory[],
800
+ currentProjectId: string
801
+ ): RetrievalResult[] {
802
+ // Filter to active action items
803
+ const actionItems = allMemories.filter(memory => {
804
+ // Must be active
805
+ if (memory.status && memory.status !== 'active') return false
806
+ if (memory.exclude_from_retrieval === true) return false
807
+
808
+ // Must be for this project or global
809
+ const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
810
+ if (!isGlobal && memory.project_id !== currentProjectId) return false
811
+
812
+ // Must have at least one action flag
813
+ return (
814
+ memory.action_required === true ||
815
+ memory.awaiting_implementation === true ||
816
+ memory.awaiting_decision === true ||
817
+ memory.context_type === 'unresolved' // unresolved = todos, blockers, open questions
818
+ )
819
+ })
820
+
821
+ // Sort by importance (desc), then by created_at (desc = newest first)
822
+ actionItems.sort((a, b) => {
823
+ const aImportance = a.importance_weight ?? 0.5
824
+ const bImportance = b.importance_weight ?? 0.5
825
+ if (bImportance !== aImportance) {
826
+ return bImportance - aImportance
827
+ }
828
+ // Newest first
829
+ const aTime = a.created_at ?? 0
830
+ const bTime = b.created_at ?? 0
831
+ return bTime - aTime
832
+ })
833
+
834
+ // Convert to result format
835
+ return actionItems.map(memory => ({
836
+ ...memory,
837
+ score: 1.0, // All action items are relevant by definition
838
+ relevance_score: 1.0,
839
+ value_score: memory.importance_weight ?? 0.5,
840
+ }))
841
+ }
package/src/core/store.ts CHANGED
@@ -534,6 +534,113 @@ export class MemoryStore {
534
534
  return id
535
535
  }
536
536
 
537
+ /**
538
+ * Update a memory's metadata fields
539
+ * Used for curation actions: promote/demote importance, bury, archive
540
+ */
541
+ async updateMemory(
542
+ projectId: string,
543
+ memoryId: string,
544
+ updates: {
545
+ importance_weight?: number
546
+ confidence_score?: number
547
+ exclude_from_retrieval?: boolean
548
+ status?: 'active' | 'pending' | 'superseded' | 'deprecated' | 'archived'
549
+ action_required?: boolean
550
+ awaiting_implementation?: boolean
551
+ awaiting_decision?: boolean
552
+ semantic_tags?: string[]
553
+ trigger_phrases?: string[]
554
+ }
555
+ ): Promise<{ success: boolean; updated_fields: string[] }> {
556
+ const { memories } = await this.getProject(projectId)
557
+
558
+ // Check memory exists
559
+ const existing = memories.get(memoryId)
560
+ if (!existing) {
561
+ return { success: false, updated_fields: [] }
562
+ }
563
+
564
+ // Build update object with only provided fields
565
+ const updateData: Record<string, any> = {}
566
+ const updatedFields: string[] = []
567
+
568
+ if (updates.importance_weight !== undefined) {
569
+ updateData.importance_weight = Math.max(0, Math.min(1, updates.importance_weight))
570
+ updatedFields.push('importance_weight')
571
+ }
572
+ if (updates.confidence_score !== undefined) {
573
+ updateData.confidence_score = Math.max(0, Math.min(1, updates.confidence_score))
574
+ updatedFields.push('confidence_score')
575
+ }
576
+ if (updates.exclude_from_retrieval !== undefined) {
577
+ updateData.exclude_from_retrieval = updates.exclude_from_retrieval
578
+ updatedFields.push('exclude_from_retrieval')
579
+ }
580
+ if (updates.status !== undefined) {
581
+ updateData.status = updates.status
582
+ updatedFields.push('status')
583
+ }
584
+ if (updates.action_required !== undefined) {
585
+ updateData.action_required = updates.action_required
586
+ updatedFields.push('action_required')
587
+ }
588
+ if (updates.awaiting_implementation !== undefined) {
589
+ updateData.awaiting_implementation = updates.awaiting_implementation
590
+ updatedFields.push('awaiting_implementation')
591
+ }
592
+ if (updates.awaiting_decision !== undefined) {
593
+ updateData.awaiting_decision = updates.awaiting_decision
594
+ updatedFields.push('awaiting_decision')
595
+ }
596
+ if (updates.semantic_tags !== undefined) {
597
+ updateData.semantic_tags = updates.semantic_tags
598
+ updatedFields.push('semantic_tags')
599
+ }
600
+ if (updates.trigger_phrases !== undefined) {
601
+ updateData.trigger_phrases = updates.trigger_phrases
602
+ updatedFields.push('trigger_phrases')
603
+ }
604
+
605
+ if (updatedFields.length === 0) {
606
+ return { success: true, updated_fields: [] }
607
+ }
608
+
609
+ // Perform update
610
+ memories.update(memoryId, updateData)
611
+
612
+ return { success: true, updated_fields: updatedFields }
613
+ }
614
+
615
+ /**
616
+ * Get a single memory by ID
617
+ */
618
+ async getMemory(projectId: string, memoryId: string): Promise<StoredMemory | null> {
619
+ const { memories } = await this.getProject(projectId)
620
+ const record = memories.get(memoryId)
621
+ if (!record) return null
622
+
623
+ return {
624
+ id: record.id,
625
+ headline: record.headline ?? '',
626
+ content: record.content,
627
+ reasoning: record.reasoning,
628
+ importance_weight: record.importance_weight,
629
+ confidence_score: record.confidence_score,
630
+ context_type: record.context_type as StoredMemory['context_type'],
631
+ status: record.status as StoredMemory['status'],
632
+ exclude_from_retrieval: record.exclude_from_retrieval,
633
+ action_required: record.action_required,
634
+ awaiting_implementation: record.awaiting_implementation,
635
+ awaiting_decision: record.awaiting_decision,
636
+ semantic_tags: record.semantic_tags,
637
+ trigger_phrases: record.trigger_phrases,
638
+ project_id: record.project_id,
639
+ created_at: record.created_at,
640
+ updated_at: record.updated_at,
641
+ } as StoredMemory
642
+ }
643
+
537
644
  /**
538
645
  * Get all memories for a project
539
646
  */
@@ -43,6 +43,7 @@ interface ContextRequest {
43
43
  current_message?: string
44
44
  max_memories?: number
45
45
  project_path?: string
46
+ mode?: 'normal' | 'action_items' // Can be set explicitly or auto-detected via ***
46
47
  }
47
48
 
48
49
  interface ProcessRequest {
@@ -136,26 +137,42 @@ export async function createServer(config: ServerConfig = {}) {
136
137
 
137
138
  logger.request('POST', '/memory/context', body.project_id)
138
139
 
140
+ // Detect *** signal at end of message for action items mode
141
+ let message = body.current_message ?? ''
142
+ let mode = body.mode ?? 'normal'
143
+
144
+ if (message.trimEnd().endsWith('***')) {
145
+ mode = 'action_items'
146
+ // Strip the *** signal from the message
147
+ message = message.trimEnd().slice(0, -3).trimEnd()
148
+ logger.debug('Action items mode detected (*** signal)', 'server')
149
+ }
150
+
139
151
  const result = await engine.getContext({
140
152
  sessionId: body.session_id,
141
153
  projectId: body.project_id,
142
- currentMessage: body.current_message ?? '',
154
+ currentMessage: message,
143
155
  maxMemories: body.max_memories,
144
156
  projectPath: body.project_path,
157
+ mode,
145
158
  })
146
159
 
147
160
  // Log what happened
148
161
  if (result.primer) {
149
162
  logger.primer(`Session primer for ${body.project_id}`)
150
163
  } else if (result.memories.length > 0) {
151
- logger.logRetrievedMemories(
152
- result.memories.map(m => ({
153
- content: m.content,
154
- score: m.score,
155
- context_type: m.context_type,
156
- })),
157
- body.current_message ?? ''
158
- )
164
+ if (mode === 'action_items') {
165
+ logger.info(`Returning ${result.memories.length} action item${result.memories.length === 1 ? '' : 's'}`)
166
+ } else {
167
+ logger.logRetrievedMemories(
168
+ result.memories.map(m => ({
169
+ content: m.content,
170
+ score: m.score,
171
+ context_type: m.context_type,
172
+ })),
173
+ message
174
+ )
175
+ }
159
176
  }
160
177
 
161
178
  return Response.json({
@@ -167,6 +184,7 @@ export async function createServer(config: ServerConfig = {}) {
167
184
  curator_enabled: true,
168
185
  memories_count: result.memories.length,
169
186
  has_primer: !!result.primer,
187
+ mode, // Include the mode that was used
170
188
  }, { headers: corsHeaders })
171
189
  }
172
190
 
@@ -369,6 +387,52 @@ export async function createServer(config: ServerConfig = {}) {
369
387
  })
370
388
  }
371
389
 
390
+ // PATCH memory - update metadata for curation (promote/demote/bury)
391
+ const patchMatch = path.match(/^\/memory\/([a-zA-Z0-9_-]+)$/)
392
+ if (patchMatch && req.method === 'PATCH') {
393
+ const memoryId = patchMatch[1]
394
+ const body = await req.json() as {
395
+ project_id: string
396
+ importance_weight?: number
397
+ confidence_score?: number
398
+ exclude_from_retrieval?: boolean
399
+ status?: 'active' | 'pending' | 'superseded' | 'deprecated' | 'archived'
400
+ action_required?: boolean
401
+ awaiting_implementation?: boolean
402
+ awaiting_decision?: boolean
403
+ semantic_tags?: string[]
404
+ trigger_phrases?: string[]
405
+ project_path?: string
406
+ }
407
+
408
+ if (!body.project_id) {
409
+ return Response.json(
410
+ { success: false, error: 'project_id is required' },
411
+ { status: 400, headers: corsHeaders }
412
+ )
413
+ }
414
+
415
+ logger.request('PATCH', `/memory/${memoryId}`, body.project_id)
416
+
417
+ const { project_id, project_path, ...updates } = body
418
+ const result = await engine.updateMemory(project_id, memoryId, updates, project_path)
419
+
420
+ if (!result.success) {
421
+ return Response.json(
422
+ { success: false, error: 'Memory not found', memory_id: memoryId },
423
+ { status: 404, headers: corsHeaders }
424
+ )
425
+ }
426
+
427
+ logger.info(`Updated memory ${memoryId}: ${result.updated_fields.join(', ')}`)
428
+
429
+ return Response.json({
430
+ success: true,
431
+ memory_id: memoryId,
432
+ updated_fields: result.updated_fields,
433
+ }, { headers: corsHeaders })
434
+ }
435
+
372
436
  // 404
373
437
  return Response.json(
374
438
  { error: 'Not found', path },