@rlabs-inc/memory 0.4.3 → 0.4.5
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 +100 -27
- package/package.json +1 -1
- package/src/cli/commands/ingest.ts +25 -2
- package/src/cli/index.ts +2 -1
- package/src/core/curator.ts +45 -3
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ A memory is relevant if **multiple signals agree** it should activate. Not coinc
|
|
|
63
63
|
|
|
64
64
|
**Phase 0 - Pre-Filter**: Exclude inactive, superseded, or wrong-scope memories
|
|
65
65
|
|
|
66
|
-
**Phase 1 - Activation Signals** (
|
|
66
|
+
**Phase 1 - Activation Signals** (7 binary signals, need ≥2 to proceed)
|
|
67
67
|
|
|
68
68
|
| Signal | Description |
|
|
69
69
|
|--------|-------------|
|
|
@@ -72,6 +72,7 @@ A memory is relevant if **multiple signals agree** it should activate. Not coinc
|
|
|
72
72
|
| Domain | Domain word found in message |
|
|
73
73
|
| Feature | Feature word found in message |
|
|
74
74
|
| Content | 3+ significant content words overlap |
|
|
75
|
+
| Files | Related file path matched in message |
|
|
75
76
|
| Vector | Semantic similarity ≥ 40% |
|
|
76
77
|
|
|
77
78
|
**Phase 2 - Importance Ranking** (among relevant memories)
|
|
@@ -88,6 +89,11 @@ A memory is relevant if **multiple signals agree** it should activate. Not coinc
|
|
|
88
89
|
|
|
89
90
|
**Selection**: Sort by signal count (DESC) → importance score (DESC). Max 2 global memories (tech prioritized), project memories fill remaining slots.
|
|
90
91
|
|
|
92
|
+
**Phase 3 - Redirects & Relationships**:
|
|
93
|
+
- If memory has `superseded_by` or `resolved_by`, surface the replacement instead
|
|
94
|
+
- Pull related memories via `blocked_by` and `blocks` fields
|
|
95
|
+
- Include `related_to` linked memories if space remains
|
|
96
|
+
|
|
91
97
|
### Global vs Project Memories
|
|
92
98
|
|
|
93
99
|
Memories are stored in two scopes:
|
|
@@ -97,6 +103,36 @@ Memories are stored in two scopes:
|
|
|
97
103
|
|
|
98
104
|
Global memories are retrieved alongside project memories, with a maximum of 2 globals per retrieval (technical types prioritized).
|
|
99
105
|
|
|
106
|
+
### Two-Tier Memory Structure
|
|
107
|
+
|
|
108
|
+
Memories are stored with a **headline + content** structure for context efficiency:
|
|
109
|
+
|
|
110
|
+
| Part | Purpose | Usage |
|
|
111
|
+
|------|---------|-------|
|
|
112
|
+
| **Headline** | 1-2 line summary, ALWAYS shown | Quick recognition |
|
|
113
|
+
| **Content** | Full structured template | Expanded on demand |
|
|
114
|
+
|
|
115
|
+
**Headline examples:**
|
|
116
|
+
```
|
|
117
|
+
Bad: "Debug session about CLI errors"
|
|
118
|
+
Good: "CLI returns error object when context full - check response.type before JSON parsing"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Type-specific content templates:**
|
|
122
|
+
|
|
123
|
+
| Type | Template |
|
|
124
|
+
|------|----------|
|
|
125
|
+
| technical | WHAT / WHERE / HOW / WHY / GOTCHA |
|
|
126
|
+
| debug | SYMPTOM / CAUSE / FIX / PREVENT |
|
|
127
|
+
| architecture | PATTERN / COMPONENTS / WHY / REJECTED |
|
|
128
|
+
| decision | DECISION / OPTIONS / REASONING / REVISIT_WHEN |
|
|
129
|
+
| personal | FACT / CONTEXT / AFFECTS |
|
|
130
|
+
| breakthrough | INSIGHT / BEFORE / AFTER / IMPLICATIONS |
|
|
131
|
+
| unresolved | QUESTION / CONTEXT / BLOCKERS / OPTIONS |
|
|
132
|
+
| state | WORKING / BROKEN / NEXT / BLOCKED_BY |
|
|
133
|
+
|
|
134
|
+
**Expansion:** Use `GET /memory/expand?ids=abc123` to fetch full content on demand.
|
|
135
|
+
|
|
100
136
|
### Smart Curation
|
|
101
137
|
At session end (or before context compaction), the same Claude instance reviews the conversation and extracts memories. No API key needed—uses Claude Code's `--resume` flag.
|
|
102
138
|
|
|
@@ -141,24 +177,21 @@ First message of each session receives temporal context:
|
|
|
141
177
|
```
|
|
142
178
|
|
|
143
179
|
### Emoji Memory Types
|
|
144
|
-
|
|
180
|
+
11 canonical types with compact visual representation:
|
|
145
181
|
|
|
146
182
|
| Emoji | Type | Meaning |
|
|
147
183
|
|-------|------|---------|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
| 🎯 | todo | Action needed |
|
|
160
|
-
| ✅ | problem_solution | Problem→Solution pair |
|
|
161
|
-
| 🏆 | milestone | Achievement |
|
|
184
|
+
| 🔧 | technical | Code, implementation, APIs |
|
|
185
|
+
| 🐛 | debug | Bugs, errors, fixes, gotchas |
|
|
186
|
+
| 🏗️ | architecture | System design, patterns |
|
|
187
|
+
| ⚖️ | decision | Choices made, trade-offs |
|
|
188
|
+
| 💜 | personal | Relationship, collaboration |
|
|
189
|
+
| 🌀 | philosophy | Beliefs, values, principles |
|
|
190
|
+
| 🔄 | workflow | How we work together |
|
|
191
|
+
| 🏆 | milestone | Achievements, shipped features |
|
|
192
|
+
| 💡 | breakthrough | Key insights, discoveries |
|
|
193
|
+
| ❓ | unresolved | Open questions, todos |
|
|
194
|
+
| 📍 | state | Current project status |
|
|
162
195
|
|
|
163
196
|
## Architecture
|
|
164
197
|
|
|
@@ -209,19 +242,21 @@ Memories are stored as human-readable markdown with YAML frontmatter:
|
|
|
209
242
|
|
|
210
243
|
```markdown
|
|
211
244
|
---
|
|
212
|
-
#
|
|
245
|
+
# Schema version
|
|
246
|
+
schema_version: 4
|
|
247
|
+
|
|
248
|
+
# Two-tier content (v4)
|
|
249
|
+
headline: "CLI returns error object when context full - check response.type"
|
|
250
|
+
# (content is in markdown body below)
|
|
251
|
+
|
|
252
|
+
# Core metadata
|
|
213
253
|
importance_weight: 0.9
|
|
214
254
|
confidence_score: 0.85
|
|
215
|
-
context_type: technical
|
|
216
|
-
temporal_relevance: persistent
|
|
255
|
+
context_type: technical # one of 11 canonical types
|
|
217
256
|
semantic_tags: [embeddings, vectors, memory-system]
|
|
218
257
|
trigger_phrases: [working with embeddings, vector search]
|
|
219
|
-
question_types: [how, what]
|
|
220
|
-
knowledge_domain: architecture
|
|
221
|
-
emotional_resonance: discovery
|
|
222
258
|
|
|
223
|
-
#
|
|
224
|
-
schema_version: 2
|
|
259
|
+
# Classification
|
|
225
260
|
status: active # active, pending, superseded, deprecated, archived
|
|
226
261
|
scope: project # global or project
|
|
227
262
|
temporal_class: long_term # eternal, long_term, medium_term, short_term, ephemeral
|
|
@@ -233,13 +268,17 @@ feature: vector-search
|
|
|
233
268
|
related_to: [memory-xyz, memory-abc]
|
|
234
269
|
supersedes: memory-old-id
|
|
235
270
|
superseded_by: null
|
|
271
|
+
blocked_by: null
|
|
272
|
+
blocks: []
|
|
236
273
|
|
|
237
274
|
# Embedding (384 dimensions)
|
|
238
275
|
embedding: [0.023, -0.041, 0.087, ...]
|
|
239
276
|
---
|
|
240
277
|
|
|
241
|
-
Embeddings are 384-dimensional vectors generated by all-MiniLM-L6-v2.
|
|
242
|
-
|
|
278
|
+
WHAT: Embeddings are 384-dimensional vectors generated by all-MiniLM-L6-v2.
|
|
279
|
+
WHERE: src/core/embeddings.ts
|
|
280
|
+
HOW: Model loads at server startup (~80MB) and generates embeddings in ~5ms.
|
|
281
|
+
WHY: Local embeddings avoid API costs and latency.
|
|
243
282
|
```
|
|
244
283
|
|
|
245
284
|
Benefits:
|
|
@@ -266,8 +305,16 @@ memory doctor --verbose # Detailed diagnostics
|
|
|
266
305
|
memory stats # Show memory statistics
|
|
267
306
|
memory stats --project x # Project-specific stats
|
|
268
307
|
|
|
269
|
-
memory migrate # Upgrade memories to latest schema
|
|
308
|
+
memory migrate # Upgrade memories to latest schema
|
|
270
309
|
memory migrate --dry-run # Preview changes without applying
|
|
310
|
+
memory migrate --analyze # Show fragmentation analysis
|
|
311
|
+
memory migrate --embeddings # Regenerate null embeddings
|
|
312
|
+
|
|
313
|
+
memory ingest # Batch ingest historical sessions
|
|
314
|
+
memory ingest --session <id> # Ingest specific session
|
|
315
|
+
memory ingest --project <name> # Ingest all sessions from project
|
|
316
|
+
memory ingest --all # Ingest all projects
|
|
317
|
+
memory ingest --dry-run # Preview what would be ingested
|
|
271
318
|
```
|
|
272
319
|
|
|
273
320
|
## Environment Variables
|
|
@@ -336,6 +383,32 @@ This isn't just about remembering facts. It's about preserving:
|
|
|
336
383
|
|
|
337
384
|
## Changelog
|
|
338
385
|
|
|
386
|
+
### v0.4.4
|
|
387
|
+
- **Docs**: Comprehensive README update with accurate v0.4.x documentation
|
|
388
|
+
- **Fix**: CLI `--version` now reads from package.json instead of hardcoded value
|
|
389
|
+
|
|
390
|
+
### v0.4.3
|
|
391
|
+
- **Fix**: Minor patch release
|
|
392
|
+
|
|
393
|
+
### v0.4.2
|
|
394
|
+
- **Feature**: Two-tier memory structure with headline + content separation
|
|
395
|
+
- **Feature**: Type-specific content templates (11 canonical types)
|
|
396
|
+
- **Feature**: `/memory/expand` endpoint for on-demand content expansion
|
|
397
|
+
- **Improvement**: Headlines enable fast recognition without full content parsing
|
|
398
|
+
|
|
399
|
+
### v0.4.1
|
|
400
|
+
- **Feature**: V3 schema with 11 canonical context types (consolidated from 170+ variants)
|
|
401
|
+
- **Feature**: 7th activation signal: `files` for related_files path matching
|
|
402
|
+
- **Feature**: Redirect logic for `superseded_by` and `resolved_by` fields
|
|
403
|
+
- **Feature**: Blocking relationships via `blocked_by` and `blocks` fields
|
|
404
|
+
- **Improvement**: Replaced `temporal_relevance` with `temporal_class` + `fade_rate`
|
|
405
|
+
- **Cleanup**: Removed 11 dead metadata fields (emotional_resonance, knowledge_domain, etc.)
|
|
406
|
+
|
|
407
|
+
### v0.4.0
|
|
408
|
+
- **Feature**: Schema versioning infrastructure for safe migrations
|
|
409
|
+
- **Feature**: Enhanced `migrate` command with `--analyze` and `--embeddings` flags
|
|
410
|
+
- **Feature**: Custom context_type mapping support for migrations
|
|
411
|
+
|
|
339
412
|
### v0.3.11
|
|
340
413
|
- **Feature**: Agent SDK integration for curator and manager - no API key needed, uses Claude Code OAuth
|
|
341
414
|
- **Feature**: `memory ingest --session <id>` to ingest a single session (useful when automatic curation fails)
|
package/package.json
CHANGED
|
@@ -306,6 +306,8 @@ export async function ingest(options: IngestOptions) {
|
|
|
306
306
|
// Accumulate all memories from this session for manager
|
|
307
307
|
const sessionMemories: CuratedMemory[] = []
|
|
308
308
|
let sessionSummary = ''
|
|
309
|
+
let interactionTone = ''
|
|
310
|
+
let projectSnapshot: CurationResult['project_snapshot'] = undefined
|
|
309
311
|
|
|
310
312
|
const spinner = new Spinner()
|
|
311
313
|
|
|
@@ -330,16 +332,36 @@ export async function ingest(options: IngestOptions) {
|
|
|
330
332
|
totalMemories++
|
|
331
333
|
}
|
|
332
334
|
|
|
333
|
-
// Keep the most recent session summary
|
|
335
|
+
// Keep the most recent session summary, tone, and snapshot
|
|
334
336
|
if (result.session_summary) {
|
|
335
337
|
sessionSummary = result.session_summary
|
|
336
338
|
}
|
|
339
|
+
if (result.interaction_tone) {
|
|
340
|
+
interactionTone = result.interaction_tone
|
|
341
|
+
}
|
|
342
|
+
if (result.project_snapshot) {
|
|
343
|
+
projectSnapshot = result.project_snapshot
|
|
344
|
+
}
|
|
337
345
|
} catch (error: any) {
|
|
338
346
|
failedSegments++
|
|
339
347
|
spinner.stop(` ${style('red', '✗')} Segment ${segment.segmentIndex + 1}/${segment.totalSegments}: ${error.message}`)
|
|
340
348
|
}
|
|
341
349
|
}
|
|
342
350
|
|
|
351
|
+
// Store session summary and project snapshot
|
|
352
|
+
if (sessionSummary) {
|
|
353
|
+
await store.storeSessionSummary(project.folderId, session.id, sessionSummary, interactionTone)
|
|
354
|
+
if (options.verbose) {
|
|
355
|
+
console.log(` ${style('dim', `Summary stored: ${sessionSummary.slice(0, 60)}...`)}`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (projectSnapshot) {
|
|
359
|
+
await store.storeProjectSnapshot(project.folderId, session.id, projectSnapshot)
|
|
360
|
+
if (options.verbose) {
|
|
361
|
+
console.log(` ${style('dim', `Snapshot stored: phase=${projectSnapshot.current_phase || 'none'}`)}`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
343
365
|
// Run manager if we have memories and manager is enabled
|
|
344
366
|
if (sessionMemories.length > 0 && managerEnabled) {
|
|
345
367
|
try {
|
|
@@ -350,7 +372,8 @@ export async function ingest(options: IngestOptions) {
|
|
|
350
372
|
const curationResult: CurationResult = {
|
|
351
373
|
memories: sessionMemories,
|
|
352
374
|
session_summary: sessionSummary,
|
|
353
|
-
|
|
375
|
+
interaction_tone: interactionTone,
|
|
376
|
+
project_snapshot: projectSnapshot,
|
|
354
377
|
}
|
|
355
378
|
|
|
356
379
|
const managerResult = await manager.manageWithSDK(
|
package/src/cli/index.ts
CHANGED
package/src/core/curator.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from 'os'
|
|
|
7
7
|
import { join } from 'path'
|
|
8
8
|
import { existsSync } from 'fs'
|
|
9
9
|
import type { CuratedMemory, CurationResult, CurationTrigger, ContextType } from '../types/memory.ts'
|
|
10
|
+
import { logger } from '../utils/logger.ts'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Get the correct Claude CLI command path
|
|
@@ -396,13 +397,14 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
396
397
|
// Try to extract JSON from response (same regex as Python)
|
|
397
398
|
const jsonMatch = responseJson.match(/\{[\s\S]*\}/)?.[0]
|
|
398
399
|
if (!jsonMatch) {
|
|
400
|
+
logger.debug('parseCurationResponse: No JSON object found in response', 'curator')
|
|
399
401
|
throw new Error('No JSON object found in response')
|
|
400
402
|
}
|
|
401
403
|
|
|
402
404
|
// Simple parse - match Python's approach
|
|
403
405
|
const data = JSON.parse(jsonMatch)
|
|
404
406
|
|
|
405
|
-
|
|
407
|
+
const result: CurationResult = {
|
|
406
408
|
session_summary: data.session_summary ?? '',
|
|
407
409
|
interaction_tone: data.interaction_tone,
|
|
408
410
|
project_snapshot: data.project_snapshot ? {
|
|
@@ -417,7 +419,13 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
417
419
|
} : undefined,
|
|
418
420
|
memories: this._parseMemories(data.memories ?? []),
|
|
419
421
|
}
|
|
420
|
-
|
|
422
|
+
|
|
423
|
+
// Log what we extracted in verbose mode
|
|
424
|
+
logger.debug(`Curator parsed: ${result.memories.length} memories, summary: ${result.session_summary ? 'yes' : 'no'}, snapshot: ${result.project_snapshot ? 'yes' : 'no'}`, 'curator')
|
|
425
|
+
|
|
426
|
+
return result
|
|
427
|
+
} catch (error: any) {
|
|
428
|
+
logger.debug(`parseCurationResponse error: ${error.message}`, 'curator')
|
|
421
429
|
return {
|
|
422
430
|
session_summary: '',
|
|
423
431
|
memories: [],
|
|
@@ -550,9 +558,17 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
550
558
|
}
|
|
551
559
|
|
|
552
560
|
if (!resultText) {
|
|
561
|
+
logger.debug('Curator SDK: No result text returned from Agent SDK', 'curator')
|
|
553
562
|
return { session_summary: '', memories: [] }
|
|
554
563
|
}
|
|
555
564
|
|
|
565
|
+
// Log raw response in verbose mode
|
|
566
|
+
logger.debug(`Curator SDK raw response (${resultText.length} chars):`, 'curator')
|
|
567
|
+
if (logger.isVerbose()) {
|
|
568
|
+
const preview = resultText.length > 3000 ? resultText.slice(0, 3000) + '...[truncated]' : resultText
|
|
569
|
+
console.log(preview)
|
|
570
|
+
}
|
|
571
|
+
|
|
556
572
|
return this.parseCurationResponse(resultText)
|
|
557
573
|
}
|
|
558
574
|
|
|
@@ -704,9 +720,21 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
704
720
|
const exitCode = await proc.exited
|
|
705
721
|
|
|
706
722
|
if (exitCode !== 0) {
|
|
723
|
+
logger.debug(`Curator CLI exited with code ${exitCode}`, 'curator')
|
|
724
|
+
if (stderr) {
|
|
725
|
+
logger.debug(`Curator stderr: ${stderr}`, 'curator')
|
|
726
|
+
}
|
|
707
727
|
return { session_summary: '', memories: [] }
|
|
708
728
|
}
|
|
709
729
|
|
|
730
|
+
// Log raw response in verbose mode
|
|
731
|
+
logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, 'curator')
|
|
732
|
+
if (logger.isVerbose()) {
|
|
733
|
+
// Show first 2000 chars to avoid flooding console
|
|
734
|
+
const preview = stdout.length > 2000 ? stdout.slice(0, 2000) + '...[truncated]' : stdout
|
|
735
|
+
console.log(preview)
|
|
736
|
+
}
|
|
737
|
+
|
|
710
738
|
// Extract JSON from CLI output
|
|
711
739
|
try {
|
|
712
740
|
// First, parse the CLI JSON wrapper
|
|
@@ -718,6 +746,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
718
746
|
// New format: array of events, find the one with type="result"
|
|
719
747
|
resultObj = cliOutput.find((item: any) => item.type === 'result')
|
|
720
748
|
if (!resultObj) {
|
|
749
|
+
logger.debug('Curator: No result object found in CLI output array', 'curator')
|
|
721
750
|
return { session_summary: '', memories: [] }
|
|
722
751
|
}
|
|
723
752
|
} else {
|
|
@@ -727,6 +756,7 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
727
756
|
|
|
728
757
|
// Check for error response FIRST (like Python does)
|
|
729
758
|
if (resultObj.type === 'error' || resultObj.is_error === true) {
|
|
759
|
+
logger.debug(`Curator: Error response from CLI: ${JSON.stringify(resultObj).slice(0, 500)}`, 'curator')
|
|
730
760
|
return { session_summary: '', memories: [] }
|
|
731
761
|
}
|
|
732
762
|
|
|
@@ -735,9 +765,17 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
735
765
|
if (typeof resultObj.result === 'string') {
|
|
736
766
|
aiResponse = resultObj.result
|
|
737
767
|
} else {
|
|
768
|
+
logger.debug(`Curator: result field is not a string: ${typeof resultObj.result}`, 'curator')
|
|
738
769
|
return { session_summary: '', memories: [] }
|
|
739
770
|
}
|
|
740
771
|
|
|
772
|
+
// Log the AI response in verbose mode
|
|
773
|
+
logger.debug(`Curator AI response (${aiResponse.length} chars):`, 'curator')
|
|
774
|
+
if (logger.isVerbose()) {
|
|
775
|
+
const preview = aiResponse.length > 3000 ? aiResponse.slice(0, 3000) + '...[truncated]' : aiResponse
|
|
776
|
+
console.log(preview)
|
|
777
|
+
}
|
|
778
|
+
|
|
741
779
|
// Remove markdown code blocks if present (```json ... ```)
|
|
742
780
|
const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
743
781
|
if (codeBlockMatch) {
|
|
@@ -747,10 +785,14 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
747
785
|
// Now find the JSON object (same regex as Python)
|
|
748
786
|
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0]
|
|
749
787
|
if (jsonMatch) {
|
|
788
|
+
logger.debug(`Curator: Found JSON object (${jsonMatch.length} chars), parsing...`, 'curator')
|
|
750
789
|
return this.parseCurationResponse(jsonMatch)
|
|
790
|
+
} else {
|
|
791
|
+
logger.debug('Curator: No JSON object found in AI response', 'curator')
|
|
751
792
|
}
|
|
752
|
-
} catch {
|
|
793
|
+
} catch (error: any) {
|
|
753
794
|
// Parse error - return empty result
|
|
795
|
+
logger.debug(`Curator: Parse error: ${error.message}`, 'curator')
|
|
754
796
|
}
|
|
755
797
|
|
|
756
798
|
return { session_summary: '', memories: [] }
|