@twelvehart/supermemory-runtime 1.0.0-next.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.
Files changed (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. package/tsconfig.json +38 -0
@@ -0,0 +1,1338 @@
1
+ /**
2
+ * Memory Service - Core Memory Operations
3
+ *
4
+ * Handles extraction, classification, and relationship detection for memories.
5
+ * This is the main service layer that orchestrates memory operations.
6
+ *
7
+ * LLM Integration: Uses LLM-based extraction when available, with automatic
8
+ * fallback to regex-based extraction if no LLM provider is configured.
9
+ *
10
+ * Note: All storage operations are delegated to the MemoryRepository.
11
+ * No in-memory caching is done here to avoid storage inconsistency.
12
+ */
13
+
14
+ import type { MemoryType, MemoryRelationship, RelationshipType, Entity } from '../types/index.js'
15
+ import { generateId } from '../utils/id.js'
16
+ import { getLogger } from '../utils/logger.js'
17
+ import { AppError, ValidationError, ErrorCode } from '../utils/errors.js'
18
+ import { validate, validateMemoryContent, containerTagSchema } from '../utils/validation.js'
19
+ import { isEmbeddingRelationshipsEnabled } from '../config/feature-flags.js'
20
+ import { getEmbeddingService, type EmbeddingService } from './embedding.service.js'
21
+ import {
22
+ Memory,
23
+ Relationship,
24
+ UpdateCheckResult,
25
+ ExtensionCheckResult,
26
+ MemoryServiceConfig,
27
+ DEFAULT_MEMORY_CONFIG,
28
+ } from './memory.types.js'
29
+ import { type MemoryRepository, getMemoryRepository } from './memory.repository.js'
30
+ import {
31
+ getLLMProvider,
32
+ isLLMAvailable,
33
+ type LLMProvider,
34
+ type LLMExtractionResult,
35
+ type LLMRelationshipResult,
36
+ LLMError,
37
+ getMemoryClassifier,
38
+ getContradictionDetector,
39
+ getMemoryExtensionDetector,
40
+ } from './llm/index.js'
41
+ import { classifyMemoryTypeHeuristically, countMemoryTypeMatches } from './llm/heuristics.js'
42
+ import { detectRelationshipsWithEmbeddings } from './relationships/detector.js'
43
+
44
+ const logger = getLogger('MemoryService')
45
+
46
+ // ============================================================================
47
+ // Relationship Detection Patterns
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Patterns indicating a memory updates or corrects previous information.
52
+ *
53
+ * @example "Actually, the deadline was moved to Friday" - update indicator
54
+ * @example "Correction: the API uses v2, not v1" - explicit correction
55
+ */
56
+ const UPDATE_INDICATOR_PATTERNS: readonly RegExp[] = [
57
+ /** Matches update/correction verbs: update, updated, correction, corrected */
58
+ /\b(?:update|updated|updating|correction|corrected)\b/i,
59
+ /** Matches correction adverbs: now, actually, instead */
60
+ /\b(?:now|actually|instead)\b/i,
61
+ /** Matches revision verbs: changed, revised, modified */
62
+ /\b(?:changed|revised|modified)\b/i,
63
+ ] as const
64
+
65
+ /**
66
+ * Patterns indicating a memory extends or adds to previous information.
67
+ *
68
+ * @example "Additionally, the API also supports batch operations"
69
+ * @example "Building on the previous point..."
70
+ */
71
+ const EXTENSION_INDICATOR_PATTERNS: readonly RegExp[] = [
72
+ /** Matches additive conjunctions: also, additionally, furthermore, moreover */
73
+ /\b(?:also|additionally|furthermore|moreover)\b/i,
74
+ /** Matches additive phrases: in addition, on top of, besides */
75
+ /\b(?:in addition|on top of|besides)\b/i,
76
+ /** Matches building phrases: extending, building on, adding to */
77
+ /\b(?:extending|building on|adding to)\b/i,
78
+ ] as const
79
+
80
+ /**
81
+ * Patterns indicating a memory is derived from or caused by another.
82
+ *
83
+ * @example "Therefore, we need to update the schema"
84
+ * @example "Based on the requirements, we chose PostgreSQL"
85
+ */
86
+ const DERIVATION_INDICATOR_PATTERNS: readonly RegExp[] = [
87
+ /** Matches consequence adverbs: therefore, thus, hence, consequently */
88
+ /\b(?:therefore|thus|hence|consequently)\b/i,
89
+ /** Matches causal conjunctions: because, since, as a result */
90
+ /\b(?:because|since|as a result)\b/i,
91
+ /** Matches derivation phrases: based on, derived from, follows from */
92
+ /\b(?:based on|derived from|follows from)\b/i,
93
+ ] as const
94
+
95
+ /**
96
+ * Patterns indicating a memory contradicts previous information.
97
+ *
98
+ * @example "However, the new tests show different results"
99
+ * @example "That's not true; the API returns JSON, not XML"
100
+ */
101
+ const CONTRADICTION_INDICATOR_PATTERNS: readonly RegExp[] = [
102
+ /** Matches contrast conjunctions: however, but, although, despite */
103
+ /\b(?:however|but|although|despite)\b/i,
104
+ /** Matches opposition words: contrary, opposite, different */
105
+ /\b(?:contrary|opposite|different)\b/i,
106
+ /** Matches negation phrases: not true, incorrect, wrong */
107
+ /\b(?:not true|incorrect|wrong)\b/i,
108
+ ] as const
109
+
110
+ /**
111
+ * Patterns indicating a memory is semantically related to another.
112
+ *
113
+ * @example "This is related to the caching discussion"
114
+ * @example "See also the authentication module docs"
115
+ */
116
+ const RELATION_INDICATOR_PATTERNS: readonly RegExp[] = [
117
+ /** Matches relation words: related, similar, like, same */
118
+ /\b(?:related|similar|like|same)\b/i,
119
+ /** Matches connection words: connected, linked, associated */
120
+ /\b(?:connected|linked|associated)\b/i,
121
+ /** Matches reference phrases: see also, refer to, compare */
122
+ /\b(?:see also|refer to|compare)\b/i,
123
+ ] as const
124
+
125
+ /**
126
+ * Patterns indicating a memory supersedes or replaces previous information.
127
+ *
128
+ * @example "This replaces the old authentication flow"
129
+ * @example "The previous approach is now deprecated"
130
+ */
131
+ const SUPERSESSION_INDICATOR_PATTERNS: readonly RegExp[] = [
132
+ /** Matches replacement verbs: replaces, supersedes, overrides */
133
+ /\b(?:replaces|supersedes|overrides)\b/i,
134
+ /** Matches obsolescence phrases: no longer, obsolete, deprecated */
135
+ /\b(?:no longer|obsolete|deprecated)\b/i,
136
+ /** Matches recency phrases: new version, latest, current */
137
+ /\b(?:new version|latest|current)\b/i,
138
+ ] as const
139
+
140
+ /**
141
+ * Combined relationship indicator patterns for relationship detection.
142
+ * Maps each RelationshipType to its corresponding regex patterns.
143
+ */
144
+ const RELATIONSHIP_INDICATORS: Record<RelationshipType, readonly RegExp[]> = {
145
+ updates: UPDATE_INDICATOR_PATTERNS,
146
+ extends: EXTENSION_INDICATOR_PATTERNS,
147
+ derives: DERIVATION_INDICATOR_PATTERNS,
148
+ contradicts: CONTRADICTION_INDICATOR_PATTERNS,
149
+ related: RELATION_INDICATOR_PATTERNS,
150
+ supersedes: SUPERSESSION_INDICATOR_PATTERNS,
151
+ }
152
+
153
+ // ============================================================================
154
+ // Entity Extraction Patterns
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Patterns for extracting person names from text.
159
+ *
160
+ * @example "Dr. John Smith" - matches honorific + name pattern
161
+ * @example "John Smith" - matches two capitalized words
162
+ */
163
+ const PERSON_ENTITY_PATTERNS: readonly RegExp[] = [
164
+ /** Matches names with honorific prefixes: Mr., Mrs., Ms., Dr., Prof. */
165
+ /\b(?:Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.)\s*[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*/g,
166
+ /** Matches two consecutive capitalized words (First Last name pattern) */
167
+ /\b[A-Z][a-z]+\s+[A-Z][a-z]+\b/g,
168
+ ] as const
169
+
170
+ /**
171
+ * Patterns for extracting place/location names from text.
172
+ *
173
+ * @example "based in San Francisco" - matches preposition + place pattern
174
+ * @example "Tokyo" - matches known major city
175
+ */
176
+ const PLACE_ENTITY_PATTERNS: readonly RegExp[] = [
177
+ /** Matches locations after prepositions: in, at, from, to */
178
+ /\b(?:in|at|from|to)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/gi,
179
+ /** Matches known major cities (extensible list) */
180
+ /\b(?:New York|Los Angeles|San Francisco|London|Paris|Tokyo|Berlin)\b/gi,
181
+ ] as const
182
+
183
+ /**
184
+ * Patterns for extracting organization names from text.
185
+ *
186
+ * @example "Acme Corp." - matches corporate suffix
187
+ * @example "Google" - matches known tech company
188
+ */
189
+ const ORGANIZATION_ENTITY_PATTERNS: readonly RegExp[] = [
190
+ /** Matches corporate suffixes: Inc., Corp., LLC, Ltd., Company, Organization */
191
+ /\b(?:Inc\.|Corp\.|LLC|Ltd\.|Company|Organization)\b/gi,
192
+ /** Matches known major tech companies (extensible list) */
193
+ /\b(?:Google|Microsoft|Apple|Amazon|Meta|OpenAI)\b/gi,
194
+ ] as const
195
+
196
+ /**
197
+ * Patterns for extracting dates from text.
198
+ *
199
+ * @example "12/25/2024" - matches numeric date format
200
+ * @example "December 25, 2024" - matches month name format
201
+ */
202
+ const DATE_ENTITY_PATTERNS: readonly RegExp[] = [
203
+ /** Matches numeric date formats: MM/DD/YYYY, DD-MM-YY, etc. */
204
+ /\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g,
205
+ /** Matches month name formats: January 15, 2024 or January 15 */
206
+ /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}(?:,?\s+\d{4})?\b/gi,
207
+ ] as const
208
+
209
+ /**
210
+ * Combined entity extraction patterns.
211
+ * Maps each entity type to its corresponding regex patterns.
212
+ */
213
+ const ENTITY_PATTERNS: Record<string, readonly RegExp[]> = {
214
+ person: PERSON_ENTITY_PATTERNS,
215
+ place: PLACE_ENTITY_PATTERNS,
216
+ organization: ORGANIZATION_ENTITY_PATTERNS,
217
+ date: DATE_ENTITY_PATTERNS,
218
+ }
219
+
220
+ // ============================================================================
221
+ // Memory Service
222
+ // ============================================================================
223
+
224
+ export class MemoryService {
225
+ private repository: MemoryRepository
226
+ private config: MemoryServiceConfig
227
+ private llmProvider: LLMProvider | null = null
228
+ private useLLM: boolean
229
+ private useEmbeddingRelationships: boolean
230
+ private embeddingService: EmbeddingService | null = null
231
+ // Note: Removed redundant `this.memories` Map to avoid dual storage inconsistency.
232
+ // All storage operations now go through the repository only.
233
+
234
+ constructor(config: Partial<MemoryServiceConfig> = {}, repository?: MemoryRepository) {
235
+ this.config = { ...DEFAULT_MEMORY_CONFIG, ...config }
236
+ this.repository = repository ?? getMemoryRepository()
237
+
238
+ // Initialize LLM provider if available
239
+ this.useLLM = isLLMAvailable()
240
+ if (this.useLLM) {
241
+ try {
242
+ this.llmProvider = getLLMProvider()
243
+ logger.info('LLM provider initialized for memory extraction', {
244
+ provider: this.llmProvider.type,
245
+ })
246
+ } catch (error) {
247
+ logger.warn('Failed to initialize LLM provider, falling back to regex', {
248
+ error: error instanceof Error ? error.message : String(error),
249
+ })
250
+ this.useLLM = false
251
+ }
252
+ } else {
253
+ logger.info('No LLM provider configured, using regex-based extraction')
254
+ }
255
+
256
+ this.useEmbeddingRelationships = isEmbeddingRelationshipsEnabled()
257
+ if (this.useEmbeddingRelationships) {
258
+ this.embeddingService = getEmbeddingService()
259
+ }
260
+
261
+ logger.debug('MemoryService initialized', {
262
+ config: this.config,
263
+ useLLM: this.useLLM,
264
+ useEmbeddings: this.useEmbeddingRelationships,
265
+ })
266
+ }
267
+
268
+ // ============================================================================
269
+ // Core API Methods (as specified in requirements)
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Extract discrete memories/facts from content
274
+ *
275
+ * Uses LLM-based extraction when available, with automatic fallback
276
+ * to regex-based extraction if LLM fails or is not configured.
277
+ *
278
+ * @param content - The text content to extract memories from
279
+ * @param options - Optional extraction options
280
+ * @returns Promise<Memory[]> - Array of extracted memories
281
+ * @throws ValidationError if content is empty or invalid
282
+ */
283
+ async extractMemories(
284
+ content: string,
285
+ options: {
286
+ containerTag?: string
287
+ minConfidence?: number
288
+ maxMemories?: number
289
+ forceLLM?: boolean
290
+ forceRegex?: boolean
291
+ } = {}
292
+ ): Promise<Memory[]> {
293
+ try {
294
+ validateMemoryContent(content)
295
+ if (options.containerTag !== undefined) {
296
+ validate(containerTagSchema, options.containerTag)
297
+ }
298
+ logger.debug('Extracting memories from content', {
299
+ contentLength: content.length,
300
+ useLLM: this.useLLM && !options.forceRegex,
301
+ })
302
+
303
+ // Determine extraction method
304
+ const shouldUseLLM = !options.forceRegex && (options.forceLLM || (this.useLLM && this.llmProvider?.isAvailable()))
305
+
306
+ if (shouldUseLLM && this.llmProvider) {
307
+ try {
308
+ return await this.extractMemoriesWithLLM(content, options)
309
+ } catch (error) {
310
+ // Log and fallback to regex
311
+ logger.warn('LLM extraction failed, falling back to regex', {
312
+ error: error instanceof Error ? error.message : String(error),
313
+ isRetryable: error instanceof LLMError ? error.retryable : false,
314
+ })
315
+ }
316
+ }
317
+
318
+ // Fallback to regex-based extraction
319
+ return this.extractMemoriesWithRegex(content, options)
320
+ } catch (error) {
321
+ if (error instanceof ValidationError) {
322
+ throw error
323
+ }
324
+ logger.errorWithException('Failed to extract memories', error)
325
+ throw AppError.from(error, ErrorCode.EXTRACTION_ERROR)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Extract memories using LLM provider
331
+ */
332
+ private async extractMemoriesWithLLM(
333
+ content: string,
334
+ options: {
335
+ containerTag?: string
336
+ minConfidence?: number
337
+ maxMemories?: number
338
+ }
339
+ ): Promise<Memory[]> {
340
+ if (!this.llmProvider) {
341
+ throw new AppError('LLM provider not available', ErrorCode.INTERNAL_ERROR)
342
+ }
343
+
344
+ const startTime = Date.now()
345
+ const result: LLMExtractionResult = await this.llmProvider.extractMemories(content, {
346
+ containerTag: options.containerTag ?? this.config.defaultContainerTag,
347
+ minConfidence: options.minConfidence ?? this.config.minConfidenceThreshold,
348
+ maxMemories: options.maxMemories,
349
+ extractEntities: true,
350
+ extractKeywords: true,
351
+ })
352
+
353
+ // Convert LLM results to Memory objects
354
+ const memories: Memory[] = result.memories.map((extracted) => ({
355
+ id: generateId(),
356
+ content: extracted.content,
357
+ type: extracted.type,
358
+ relationships: [],
359
+ isLatest: true,
360
+ containerTag: options.containerTag ?? this.config.defaultContainerTag,
361
+ sourceContent: content.substring(0, 500),
362
+ confidence: extracted.confidence,
363
+ metadata: {
364
+ confidence: extracted.confidence,
365
+ extractedFrom: content.substring(0, 100),
366
+ keywords: extracted.keywords,
367
+ entities: extracted.entities,
368
+ extractionMethod: 'llm',
369
+ classificationMethod: 'llm',
370
+ llmProvider: result.provider,
371
+ tokensUsed: result.tokensUsed?.total,
372
+ },
373
+ createdAt: new Date(),
374
+ updatedAt: new Date(),
375
+ }))
376
+
377
+ logger.info('Memories extracted with LLM', {
378
+ count: memories.length,
379
+ provider: result.provider,
380
+ cached: result.cached,
381
+ processingTimeMs: Date.now() - startTime,
382
+ tokensUsed: result.tokensUsed?.total,
383
+ })
384
+
385
+ return memories
386
+ }
387
+
388
+ /**
389
+ * Extract memories using regex-based patterns (fallback)
390
+ */
391
+ private extractMemoriesWithRegex(
392
+ content: string,
393
+ options: {
394
+ containerTag?: string
395
+ minConfidence?: number
396
+ maxMemories?: number
397
+ }
398
+ ): Memory[] {
399
+ const sentences = this.splitIntoSentences(content)
400
+ const memories: Memory[] = []
401
+ const maxMemories = options.maxMemories ?? 50
402
+ const minConfidence = options.minConfidence ?? this.config.minConfidenceThreshold
403
+
404
+ for (const sentence of sentences) {
405
+ if (memories.length >= maxMemories) break
406
+ if (sentence.trim().length < 10) continue
407
+
408
+ const type = this.classifyMemoryType(sentence)
409
+ const entities = this.extractEntities(sentence)
410
+ const keywords = this.extractKeywords(sentence)
411
+ const confidence = this.calculateConfidence(sentence, type)
412
+
413
+ if (confidence < minConfidence) continue
414
+
415
+ const memory: Memory = {
416
+ id: generateId(),
417
+ content: sentence.trim(),
418
+ type,
419
+ relationships: [],
420
+ isLatest: true,
421
+ containerTag: options.containerTag ?? this.config.defaultContainerTag,
422
+ sourceContent: content.substring(0, 500),
423
+ confidence,
424
+ metadata: {
425
+ confidence,
426
+ extractedFrom: content.substring(0, 100),
427
+ keywords,
428
+ entities,
429
+ extractionMethod: 'regex',
430
+ classificationMethod: 'heuristic',
431
+ },
432
+ createdAt: new Date(),
433
+ updatedAt: new Date(),
434
+ }
435
+
436
+ memories.push(memory)
437
+ }
438
+
439
+ logger.info('Memories extracted with regex', { count: memories.length })
440
+ return memories
441
+ }
442
+
443
+ /**
444
+ * Detect relationships between a new memory and existing memories
445
+ *
446
+ * Uses LLM-based detection when available, with automatic fallback
447
+ * to pattern-based detection if LLM fails or is not configured.
448
+ *
449
+ * @param newMemory - The new memory to check
450
+ * @param existingMemories - Array of existing memories to compare against
451
+ * @param options - Optional detection options
452
+ * @returns Promise<Relationship[]> - Array of detected relationships
453
+ */
454
+ async detectRelationshipsAsync(
455
+ newMemory: Memory,
456
+ existingMemories: Memory[],
457
+ options: {
458
+ minConfidence?: number
459
+ maxRelationships?: number
460
+ forceLLM?: boolean
461
+ forceRegex?: boolean
462
+ } = {}
463
+ ): Promise<Relationship[]> {
464
+ // Limit comparisons for performance
465
+ const memoriesToCompare = existingMemories.slice(0, this.config.maxRelationshipComparisons)
466
+
467
+ if (memoriesToCompare.length === 0) {
468
+ return []
469
+ }
470
+
471
+ const shouldUseLLM = !options.forceRegex && (options.forceLLM || (this.useLLM && this.llmProvider?.isAvailable()))
472
+
473
+ if (shouldUseLLM && this.llmProvider) {
474
+ try {
475
+ return await this.detectRelationshipsWithLLM(newMemory, memoriesToCompare, options)
476
+ } catch (error) {
477
+ logger.warn('LLM relationship detection failed, falling back to patterns', {
478
+ error: error instanceof Error ? error.message : String(error),
479
+ })
480
+ }
481
+ }
482
+
483
+ // Fallback to pattern-based detection
484
+ return this.detectRelationships(newMemory, memoriesToCompare)
485
+ }
486
+
487
+ /**
488
+ * Detect relationships using LLM provider
489
+ */
490
+ private async detectRelationshipsWithLLM(
491
+ newMemory: Memory,
492
+ existingMemories: Memory[],
493
+ options: {
494
+ minConfidence?: number
495
+ maxRelationships?: number
496
+ }
497
+ ): Promise<Relationship[]> {
498
+ if (!this.llmProvider) {
499
+ throw new AppError('LLM provider not available', ErrorCode.INTERNAL_ERROR)
500
+ }
501
+
502
+ const result: LLMRelationshipResult = await this.llmProvider.detectRelationships(
503
+ { id: newMemory.id, content: newMemory.content, type: newMemory.type },
504
+ existingMemories.map((m) => ({ id: m.id, content: m.content, type: m.type })),
505
+ {
506
+ minConfidence: options.minConfidence ?? 0.5,
507
+ maxRelationships: options.maxRelationships,
508
+ }
509
+ )
510
+
511
+ // Convert LLM results to Relationship objects
512
+ const relationships: Relationship[] = result.relationships.map((rel) => ({
513
+ id: generateId(),
514
+ sourceMemoryId: rel.sourceMemoryId,
515
+ targetMemoryId: rel.targetMemoryId,
516
+ type: rel.type,
517
+ confidence: rel.confidence,
518
+ description: rel.reason,
519
+ createdAt: new Date(),
520
+ metadata: {
521
+ detectionMethod: 'llm',
522
+ llmProvider: result.provider,
523
+ },
524
+ }))
525
+
526
+ logger.info('Relationships detected with LLM', {
527
+ count: relationships.length,
528
+ supersededCount: result.supersededMemoryIds.length,
529
+ provider: result.provider,
530
+ processingTimeMs: result.processingTimeMs,
531
+ })
532
+
533
+ return relationships
534
+ }
535
+
536
+ /**
537
+ * Detect relationships using pattern-based heuristics (synchronous, for backwards compatibility)
538
+ *
539
+ * @param newMemory - The new memory to check
540
+ * @param existingMemories - Array of existing memories to compare against
541
+ * @returns Relationship[] - Array of detected relationships
542
+ */
543
+ detectRelationships(newMemory: Memory, existingMemories: Memory[]): Relationship[] {
544
+ const relationships: Relationship[] = []
545
+
546
+ // Limit comparisons for performance
547
+ const memoriesToCompare = existingMemories.slice(0, this.config.maxRelationshipComparisons)
548
+
549
+ for (const existing of memoriesToCompare) {
550
+ if (existing.id === newMemory.id) continue
551
+
552
+ // Check for updates (new memory supersedes old)
553
+ const updateResult = this.checkForUpdates(newMemory, existing)
554
+ if (updateResult.isUpdate && updateResult.confidence >= 0.7) {
555
+ relationships.push({
556
+ id: generateId(),
557
+ sourceMemoryId: newMemory.id,
558
+ targetMemoryId: existing.id,
559
+ type: 'updates',
560
+ confidence: updateResult.confidence,
561
+ description: updateResult.reason,
562
+ createdAt: new Date(),
563
+ metadata: { detectionMethod: 'pattern' },
564
+ })
565
+ continue
566
+ }
567
+
568
+ // Check for extensions (new memory adds to old)
569
+ const extensionResult = this.checkForExtensions(newMemory, existing)
570
+ if (extensionResult.isExtension && extensionResult.confidence >= 0.6) {
571
+ relationships.push({
572
+ id: generateId(),
573
+ sourceMemoryId: newMemory.id,
574
+ targetMemoryId: existing.id,
575
+ type: 'extends',
576
+ confidence: extensionResult.confidence,
577
+ description: extensionResult.reason,
578
+ createdAt: new Date(),
579
+ metadata: { detectionMethod: 'pattern' },
580
+ })
581
+ continue
582
+ }
583
+
584
+ // Check for general semantic relationship
585
+ const similarity = this.calculateTextSimilarity(newMemory.content, existing.content)
586
+ if (similarity >= 0.5) {
587
+ relationships.push({
588
+ id: generateId(),
589
+ sourceMemoryId: newMemory.id,
590
+ targetMemoryId: existing.id,
591
+ type: 'related',
592
+ confidence: similarity,
593
+ description: 'Semantically related content',
594
+ createdAt: new Date(),
595
+ metadata: { detectionMethod: 'pattern' },
596
+ })
597
+ }
598
+ }
599
+
600
+ return relationships
601
+ }
602
+
603
+ /**
604
+ * Detect relationships using the default path selection.
605
+ * Embedding detection is used only when explicitly enabled.
606
+ */
607
+ private async detectRelationshipsForMemory(
608
+ newMemory: Memory,
609
+ existingMemories: Memory[],
610
+ containerTag?: string
611
+ ): Promise<Relationship[]> {
612
+ if (!this.useEmbeddingRelationships || !this.embeddingService) {
613
+ return this.detectRelationships(newMemory, existingMemories)
614
+ }
615
+
616
+ if (existingMemories.length === 0) {
617
+ return []
618
+ }
619
+
620
+ try {
621
+ const result = await detectRelationshipsWithEmbeddings(newMemory, existingMemories, this.embeddingService, {
622
+ containerTag,
623
+ config: {
624
+ maxCandidates: this.config.maxRelationshipComparisons,
625
+ },
626
+ })
627
+ return result.relationships.map((rel) => rel.relationship)
628
+ } catch (error) {
629
+ logger.warn('Embedding relationship detection failed, falling back to patterns', {
630
+ error: error instanceof Error ? error.message : String(error),
631
+ })
632
+ return this.detectRelationships(newMemory, existingMemories)
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Classify the type of memory from content
638
+ *
639
+ * @param content - The content to classify
640
+ * @returns MemoryType - 'fact' | 'preference' | 'episode' (mapped to full type set)
641
+ */
642
+ classifyMemoryType(content: string): MemoryType {
643
+ // Use LLM-based classification service with pattern matching fallback
644
+ // This replaces the TODO-001 implementation
645
+ // Note: This is synchronous for backward compatibility.
646
+ // For LLM async, call: await getMemoryClassifier().classify(content)
647
+
648
+ const heuristic = classifyMemoryTypeHeuristically(content)
649
+ return heuristic.type
650
+ }
651
+
652
+ /**
653
+ * Classify memory type asynchronously using LLM (preferred method)
654
+ *
655
+ * @param content - The content to classify
656
+ * @returns Promise with MemoryType
657
+ */
658
+ async classifyMemoryTypeAsync(content: string): Promise<MemoryType> {
659
+ const classifier = getMemoryClassifier()
660
+ const result = await classifier.classify(content)
661
+ return result.type
662
+ }
663
+
664
+ /**
665
+ * Check if a new memory updates/supersedes an existing memory (contradiction check)
666
+ *
667
+ * @param newMemory - The new memory
668
+ * @param existing - The existing memory to compare
669
+ * @returns UpdateCheckResult
670
+ */
671
+ checkForUpdates(newMemory: Memory, existing: Memory): UpdateCheckResult {
672
+ // Use heuristic fallback for synchronous calls
673
+ // For LLM-based detection, use checkForUpdatesAsync instead
674
+ const newLower = newMemory.content.toLowerCase()
675
+ const existingLower = existing.content.toLowerCase()
676
+
677
+ const newWords = new Set(newLower.split(/\s+/).filter((w) => w.length > 3))
678
+ const existingWords = new Set(existingLower.split(/\s+/).filter((w) => w.length > 3))
679
+
680
+ const intersection = new Set([...newWords].filter((x) => existingWords.has(x)))
681
+ const overlapRatio = intersection.size / Math.min(newWords.size, existingWords.size) || 0
682
+
683
+ let hasUpdateIndicator = false
684
+ for (const pattern of RELATIONSHIP_INDICATORS.updates) {
685
+ if (pattern.test(newLower)) {
686
+ hasUpdateIndicator = true
687
+ break
688
+ }
689
+ }
690
+
691
+ let hasContradiction = false
692
+ for (const pattern of RELATIONSHIP_INDICATORS.contradicts) {
693
+ if (pattern.test(newLower) && overlapRatio > 0.3) {
694
+ hasContradiction = true
695
+ break
696
+ }
697
+ }
698
+
699
+ let hasSuperseding = false
700
+ for (const pattern of RELATIONSHIP_INDICATORS.supersedes) {
701
+ if (pattern.test(newLower) && overlapRatio > 0.4) {
702
+ hasSuperseding = true
703
+ break
704
+ }
705
+ }
706
+
707
+ const isUpdate = (hasUpdateIndicator || hasContradiction || hasSuperseding) && overlapRatio > 0.3
708
+ const confidence = isUpdate ? Math.min(0.9, overlapRatio + 0.3) : 0
709
+
710
+ let reason = 'No update relationship detected'
711
+ if (isUpdate) {
712
+ if (hasContradiction) {
713
+ reason = 'New memory contradicts existing information'
714
+ } else if (hasSuperseding) {
715
+ reason = 'New memory supersedes existing information'
716
+ } else {
717
+ reason = 'New memory updates existing information'
718
+ }
719
+ }
720
+
721
+ return {
722
+ isUpdate,
723
+ existingMemory: isUpdate ? existing : undefined,
724
+ confidence,
725
+ reason,
726
+ }
727
+ }
728
+
729
+ /**
730
+ * Check for updates/contradictions asynchronously using LLM (preferred method)
731
+ * Replaces TODO-002 with semantic analysis
732
+ *
733
+ * @param newMemory - The new memory
734
+ * @param existing - The existing memory to compare
735
+ * @returns Promise with UpdateCheckResult
736
+ */
737
+ async checkForUpdatesAsync(newMemory: Memory, existing: Memory): Promise<UpdateCheckResult> {
738
+ const detector = getContradictionDetector()
739
+ const result = await detector.checkContradiction(newMemory, existing)
740
+
741
+ return {
742
+ isUpdate: result.isContradiction,
743
+ existingMemory: result.isContradiction ? existing : undefined,
744
+ confidence: result.confidence,
745
+ reason: result.reason,
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Check if a new memory extends/enriches an existing memory
751
+ *
752
+ * @param newMemory - The new memory
753
+ * @param existing - The existing memory to compare
754
+ * @returns ExtensionCheckResult
755
+ */
756
+ checkForExtensions(newMemory: Memory, existing: Memory): ExtensionCheckResult {
757
+ // TODO: Replace with actual LLM call for extension detection
758
+ // Example LLM prompt:
759
+ // ```
760
+ // Compare these two statements and determine if the NEW statement
761
+ // extends or adds detail to the OLD statement (without contradicting):
762
+ //
763
+ // OLD: ${existing.content}
764
+ // NEW: ${newMemory.content}
765
+ //
766
+ // Return JSON: { isExtension: boolean, confidence: 0-1, reason: string }
767
+ // ```
768
+
769
+ const newLower = newMemory.content.toLowerCase()
770
+ const existingLower = existing.content.toLowerCase()
771
+
772
+ // Check for common subject matter
773
+ const newWords = newLower.split(/\s+/).filter((w) => w.length > 3)
774
+ const existingWords = new Set(existingLower.split(/\s+/).filter((w) => w.length > 3))
775
+
776
+ const commonWords = newWords.filter((w) => existingWords.has(w))
777
+ const overlapRatio = commonWords.length / Math.min(newWords.length, existingWords.size) || 0
778
+
779
+ // New memory should be longer or contain additional information
780
+ const hasMoreDetail = newMemory.content.length > existing.content.length * 0.8
781
+
782
+ // Extension indicators
783
+ let hasExtensionIndicator = false
784
+ for (const pattern of RELATIONSHIP_INDICATORS.extends) {
785
+ if (pattern.test(newLower)) {
786
+ hasExtensionIndicator = true
787
+ break
788
+ }
789
+ }
790
+
791
+ // Check if new content is contained within old (not an extension)
792
+ const newContentInOld = existingLower.includes(newLower.slice(0, 20))
793
+
794
+ const isExtension =
795
+ overlapRatio > 0.2 && overlapRatio < 0.9 && !newContentInOld && (hasMoreDetail || hasExtensionIndicator)
796
+
797
+ const confidence = isExtension ? Math.min(0.85, overlapRatio + 0.2) : 0
798
+
799
+ return {
800
+ isExtension,
801
+ existingMemory: isExtension ? existing : undefined,
802
+ confidence,
803
+ reason: isExtension
804
+ ? 'New memory adds additional detail to existing information'
805
+ : 'No extension relationship detected',
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Check for extensions asynchronously using LLM (preferred method)
811
+ * Replaces TODO-003 with semantic analysis
812
+ *
813
+ * @param newMemory - The new memory
814
+ * @param existing - The existing memory to compare
815
+ * @returns Promise with ExtensionCheckResult
816
+ */
817
+ async checkForExtensionsAsync(newMemory: Memory, existing: Memory): Promise<ExtensionCheckResult> {
818
+ const detector = getMemoryExtensionDetector()
819
+ const result = await detector.checkExtension(newMemory, existing)
820
+
821
+ return {
822
+ isExtension: result.isExtension,
823
+ existingMemory: result.isExtension ? existing : undefined,
824
+ confidence: result.confidence,
825
+ reason: result.reason,
826
+ }
827
+ }
828
+
829
+ // ============================================================================
830
+ // Extended API Methods
831
+ // ============================================================================
832
+
833
+ /**
834
+ * Process content and store memories with automatic relationship detection
835
+ *
836
+ * @throws ValidationError if content or containerTag is invalid
837
+ */
838
+ async processAndStoreMemories(
839
+ content: string,
840
+ options: {
841
+ containerTag?: string
842
+ sourceId?: string
843
+ detectRelationships?: boolean
844
+ } = {}
845
+ ): Promise<{
846
+ memories: Memory[]
847
+ relationships: Relationship[]
848
+ supersededMemoryIds: string[]
849
+ }> {
850
+ const createdMemoryIds: string[] = []
851
+ const relationshipIdsToRollback: string[] = []
852
+ const supersedeSnapshots: Array<{
853
+ id: string
854
+ isLatest: boolean
855
+ supersededBy?: string
856
+ }> = []
857
+
858
+ const rollback = async (reason: unknown) => {
859
+ logger.warn('Rolling back processAndStoreMemories due to failure', {
860
+ error: reason instanceof Error ? reason.message : String(reason),
861
+ })
862
+
863
+ for (const snapshot of supersedeSnapshots) {
864
+ try {
865
+ const existing = await this.repository.findById(snapshot.id)
866
+ if (existing) {
867
+ await this.repository.update(snapshot.id, {
868
+ isLatest: snapshot.isLatest,
869
+ supersededBy: snapshot.supersededBy,
870
+ })
871
+ }
872
+ } catch (error) {
873
+ logger.warn('Failed to rollback superseded memory', {
874
+ memoryId: snapshot.id,
875
+ error: error instanceof Error ? error.message : String(error),
876
+ })
877
+ }
878
+ }
879
+
880
+ for (const relId of relationshipIdsToRollback) {
881
+ try {
882
+ await this.repository.deleteRelationship(relId)
883
+ } catch (error) {
884
+ logger.warn('Failed to rollback relationship', {
885
+ relationshipId: relId,
886
+ error: error instanceof Error ? error.message : String(error),
887
+ })
888
+ }
889
+ }
890
+
891
+ for (const memoryId of createdMemoryIds) {
892
+ try {
893
+ await this.repository.delete(memoryId)
894
+ } catch (error) {
895
+ logger.warn('Failed to rollback memory', {
896
+ memoryId,
897
+ error: error instanceof Error ? error.message : String(error),
898
+ })
899
+ }
900
+ }
901
+ }
902
+
903
+ try {
904
+ // Only use default containerTag if not explicitly provided (including undefined)
905
+ const containerTag = 'containerTag' in options ? options.containerTag : this.config.defaultContainerTag
906
+
907
+ if (containerTag) {
908
+ validate(containerTagSchema, containerTag)
909
+ }
910
+
911
+ const shouldDetectRelationships = options.detectRelationships ?? this.config.autoDetectRelationships
912
+
913
+ logger.debug('Processing and storing memories', {
914
+ containerTag,
915
+ detectRelationships: shouldDetectRelationships,
916
+ })
917
+
918
+ // Extract memories from content
919
+ const extractedMemories = await this.extractMemories(content)
920
+
921
+ // Update container tags and source info
922
+ for (const memory of extractedMemories) {
923
+ memory.containerTag = containerTag
924
+ if (options.sourceId) {
925
+ memory.sourceId = options.sourceId
926
+ }
927
+ }
928
+
929
+ const allRelationships: Relationship[] = []
930
+ const supersededMemoryIds: string[] = []
931
+
932
+ // Process each extracted memory
933
+ for (const memory of extractedMemories) {
934
+ // Store the memory in repository only (no local cache)
935
+ await this.repository.create(memory)
936
+ createdMemoryIds.push(memory.id)
937
+
938
+ // Detect relationships if enabled
939
+ if (shouldDetectRelationships) {
940
+ const existingMemories = await this.repository.findPotentialRelations(memory, {
941
+ containerTag,
942
+ limit: this.config.maxRelationshipComparisons,
943
+ })
944
+
945
+ const relationships = await this.detectRelationshipsForMemory(memory, existingMemories, containerTag)
946
+
947
+ // Set relationship detection method in memory metadata
948
+ const relationshipMethod = this.useEmbeddingRelationships && this.embeddingService ? 'embedding' : 'heuristic'
949
+ memory.metadata.relationshipMethod = relationshipMethod
950
+
951
+ const existingById = new Map(existingMemories.map((m) => [m.id, m]))
952
+
953
+ // Process update relationships - mark old memories as superseded
954
+ for (const rel of relationships) {
955
+ if (rel.type === 'updates' || rel.type === 'supersedes') {
956
+ const target = existingById.get(rel.targetMemoryId)
957
+ if (target && memory.containerTag && target.containerTag && memory.containerTag !== target.containerTag) {
958
+ continue
959
+ }
960
+ if (target) {
961
+ supersedeSnapshots.push({
962
+ id: target.id,
963
+ isLatest: target.isLatest,
964
+ supersededBy: target.supersededBy,
965
+ })
966
+ }
967
+ await this.repository.markSuperseded(rel.targetMemoryId, memory.id)
968
+ supersededMemoryIds.push(rel.targetMemoryId)
969
+ }
970
+ }
971
+
972
+ // Store relationships
973
+ if (relationships.length > 0) {
974
+ relationshipIdsToRollback.push(...relationships.map((rel) => rel.id))
975
+ await this.repository.createRelationshipBatch(relationships)
976
+ allRelationships.push(...relationships)
977
+ }
978
+ }
979
+ }
980
+
981
+ logger.info('Memories processed and stored', {
982
+ memoriesCount: extractedMemories.length,
983
+ relationshipsCount: allRelationships.length,
984
+ supersededCount: supersededMemoryIds.length,
985
+ })
986
+
987
+ return {
988
+ memories: extractedMemories,
989
+ relationships: allRelationships,
990
+ supersededMemoryIds,
991
+ }
992
+ } catch (error) {
993
+ await rollback(error)
994
+ if (error instanceof AppError) {
995
+ throw error
996
+ }
997
+ logger.errorWithException('Failed to process and store memories', error)
998
+ throw AppError.from(error, ErrorCode.INTERNAL_ERROR)
999
+ }
1000
+ }
1001
+
1002
+ /**
1003
+ * Update isLatest status when new memory supersedes existing ones
1004
+ */
1005
+ updateIsLatest(newMemory: Memory, existingMemories: Memory[]): void {
1006
+ for (const existing of existingMemories) {
1007
+ if (newMemory.containerTag && existing.containerTag && newMemory.containerTag !== existing.containerTag) {
1008
+ continue
1009
+ }
1010
+ const updateResult = this.checkForUpdates(newMemory, existing)
1011
+ if (updateResult.isUpdate && updateResult.confidence >= 0.7) {
1012
+ existing.isLatest = false
1013
+ existing.supersededBy = newMemory.id
1014
+ newMemory.relationships.push({
1015
+ type: 'supersedes',
1016
+ targetId: existing.id,
1017
+ confidence: updateResult.confidence,
1018
+ })
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ /**
1024
+ * Extract memories from text (convenience wrapper matching original API)
1025
+ *
1026
+ * @throws ValidationError if text or containerTag is invalid
1027
+ */
1028
+ extractMemoriesFromText(text: string, containerTag?: string): Memory[] {
1029
+ validateMemoryContent(text)
1030
+ if (containerTag) {
1031
+ validate(containerTagSchema, containerTag)
1032
+ }
1033
+
1034
+ const sentences = this.splitIntoSentences(text)
1035
+ const memories: Memory[] = []
1036
+
1037
+ for (const sentence of sentences) {
1038
+ if (sentence.trim().length < 10) continue
1039
+
1040
+ const type = this.classifyMemoryType(sentence)
1041
+ const entities = this.extractEntities(sentence)
1042
+ const keywords = this.extractKeywords(sentence)
1043
+ const confidence = this.calculateConfidence(sentence, type)
1044
+
1045
+ const memory: Memory = {
1046
+ id: generateId(),
1047
+ content: sentence.trim(),
1048
+ type,
1049
+ relationships: [],
1050
+ isLatest: true,
1051
+ containerTag: containerTag ?? this.config.defaultContainerTag,
1052
+ confidence,
1053
+ metadata: {
1054
+ confidence,
1055
+ extractedFrom: text.substring(0, 100),
1056
+ keywords,
1057
+ entities,
1058
+ },
1059
+ createdAt: new Date(),
1060
+ updatedAt: new Date(),
1061
+ }
1062
+
1063
+ memories.push(memory)
1064
+ }
1065
+
1066
+ // Detect relationships between extracted memories
1067
+ this.detectRelationshipsInternal(memories)
1068
+
1069
+ return memories
1070
+ }
1071
+
1072
+ // ============================================================================
1073
+ // Storage Methods (delegating to repository)
1074
+ // ============================================================================
1075
+
1076
+ async storeMemory(memory: Memory): Promise<Memory> {
1077
+ return this.repository.create(memory)
1078
+ }
1079
+
1080
+ async getMemory(id: string): Promise<Memory | null> {
1081
+ return this.repository.findById(id)
1082
+ }
1083
+
1084
+ async getAllMemories(): Promise<Memory[]> {
1085
+ return this.repository.getAllMemories()
1086
+ }
1087
+
1088
+ async getLatestMemories(): Promise<Memory[]> {
1089
+ const all = await this.repository.getAllMemories()
1090
+ return all.filter((m) => m.isLatest)
1091
+ }
1092
+
1093
+ // ============================================================================
1094
+ // Private Helper Methods
1095
+ // ============================================================================
1096
+
1097
+ private splitIntoSentences(text: string): string[] {
1098
+ return text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0)
1099
+ }
1100
+
1101
+ private extractEntities(text: string): Entity[] {
1102
+ const entities: Entity[] = []
1103
+ const seen = new Set<string>()
1104
+
1105
+ for (const [type, patterns] of Object.entries(ENTITY_PATTERNS)) {
1106
+ for (const pattern of patterns) {
1107
+ const matches = text.matchAll(pattern)
1108
+ for (const match of matches) {
1109
+ const name = match[1] || match[0]
1110
+ const normalizedName = name.trim().toLowerCase()
1111
+
1112
+ if (!seen.has(normalizedName) && name.length > 1) {
1113
+ seen.add(normalizedName)
1114
+ entities.push({
1115
+ name: name.trim(),
1116
+ type: type as Entity['type'],
1117
+ mentions: 1,
1118
+ })
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ return entities
1125
+ }
1126
+
1127
+ private extractKeywords(text: string): string[] {
1128
+ const stopWords = new Set([
1129
+ 'the',
1130
+ 'a',
1131
+ 'an',
1132
+ 'and',
1133
+ 'or',
1134
+ 'but',
1135
+ 'in',
1136
+ 'on',
1137
+ 'at',
1138
+ 'to',
1139
+ 'for',
1140
+ 'of',
1141
+ 'with',
1142
+ 'by',
1143
+ 'from',
1144
+ 'as',
1145
+ 'is',
1146
+ 'was',
1147
+ 'are',
1148
+ 'were',
1149
+ 'been',
1150
+ 'be',
1151
+ 'have',
1152
+ 'has',
1153
+ 'had',
1154
+ 'do',
1155
+ 'does',
1156
+ 'did',
1157
+ 'will',
1158
+ 'would',
1159
+ 'could',
1160
+ 'should',
1161
+ 'may',
1162
+ 'might',
1163
+ 'must',
1164
+ 'shall',
1165
+ 'can',
1166
+ 'need',
1167
+ 'it',
1168
+ 'this',
1169
+ 'that',
1170
+ 'these',
1171
+ 'those',
1172
+ 'i',
1173
+ 'you',
1174
+ 'he',
1175
+ 'she',
1176
+ 'we',
1177
+ 'they',
1178
+ 'my',
1179
+ 'your',
1180
+ 'his',
1181
+ 'her',
1182
+ 'our',
1183
+ 'their',
1184
+ 'its',
1185
+ ])
1186
+
1187
+ const words = text.toLowerCase().match(/\b[a-z]{3,}\b/g) || []
1188
+ const keywords = words.filter((word) => !stopWords.has(word))
1189
+
1190
+ return [...new Set(keywords)].slice(0, 10)
1191
+ }
1192
+
1193
+ private calculateConfidence(content: string, type: MemoryType): number {
1194
+ let confidence = 0.5
1195
+
1196
+ // Longer content with more detail = higher confidence
1197
+ if (content.length > 100) confidence += 0.1
1198
+ if (content.length > 200) confidence += 0.1
1199
+
1200
+ // Pattern matches increase confidence
1201
+ const matchCount = countMemoryTypeMatches(content, type)
1202
+ confidence += Math.min(matchCount * 0.1, 0.2)
1203
+
1204
+ return Math.min(confidence, 1)
1205
+ }
1206
+
1207
+ private calculateTextSimilarity(text1: string, text2: string): number {
1208
+ const words1 = new Set(text1.toLowerCase().split(/\s+/))
1209
+ const words2 = new Set(text2.toLowerCase().split(/\s+/))
1210
+
1211
+ const intersection = new Set([...words1].filter((x) => words2.has(x)))
1212
+ const union = new Set([...words1, ...words2])
1213
+
1214
+ if (union.size === 0) return 0
1215
+ return intersection.size / union.size
1216
+ }
1217
+
1218
+ private detectRelationshipsInternal(memories: Memory[]): MemoryRelationship[] {
1219
+ const relationships: MemoryRelationship[] = []
1220
+
1221
+ for (let i = 0; i < memories.length; i++) {
1222
+ for (let j = i + 1; j < memories.length; j++) {
1223
+ const sourceMemory = memories[i]!
1224
+ const targetMemory = memories[j]!
1225
+
1226
+ const relationshipType = this.detectRelationshipType(sourceMemory.content, targetMemory.content)
1227
+
1228
+ if (relationshipType) {
1229
+ const relationship: MemoryRelationship = {
1230
+ type: relationshipType,
1231
+ targetId: targetMemory.id,
1232
+ confidence: this.calculateRelationshipConfidence(sourceMemory, targetMemory, relationshipType),
1233
+ }
1234
+
1235
+ sourceMemory.relationships.push(relationship)
1236
+ relationships.push(relationship)
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ return relationships
1242
+ }
1243
+
1244
+ private detectRelationshipType(source: string, target: string): RelationshipType | null {
1245
+ const similarity = this.calculateTextSimilarity(source, target)
1246
+ if (similarity < 0.1) {
1247
+ return null
1248
+ }
1249
+
1250
+ // Check explicit relationship indicators
1251
+ for (const [type, patterns] of Object.entries(RELATIONSHIP_INDICATORS)) {
1252
+ for (const pattern of patterns) {
1253
+ if (pattern.test(source) || pattern.test(target)) {
1254
+ return type as RelationshipType
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ // If similar but no explicit indicator, mark as related
1260
+ if (similarity > 0.3) {
1261
+ return 'related'
1262
+ }
1263
+
1264
+ return null
1265
+ }
1266
+
1267
+ private calculateRelationshipConfidence(source: Memory, target: Memory, type: RelationshipType): number {
1268
+ let confidence = 0.5
1269
+
1270
+ // Same container increases confidence
1271
+ if (source.containerTag && source.containerTag === target.containerTag) {
1272
+ confidence += 0.1
1273
+ }
1274
+
1275
+ // Text similarity affects confidence
1276
+ const similarity = this.calculateTextSimilarity(source.content, target.content)
1277
+ confidence += similarity * 0.3
1278
+
1279
+ // Explicit indicators increase confidence
1280
+ const patterns = RELATIONSHIP_INDICATORS[type]
1281
+ if (patterns) {
1282
+ for (const pattern of patterns) {
1283
+ if (pattern.test(source.content) || pattern.test(target.content)) {
1284
+ confidence += 0.1
1285
+ break
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ return Math.min(confidence, 1)
1291
+ }
1292
+ }
1293
+
1294
+ // ============================================================================
1295
+ // Factory Functions (Proxy-based Lazy Singleton)
1296
+ // ============================================================================
1297
+
1298
+ let _serviceInstance: MemoryService | null = null
1299
+
1300
+ /**
1301
+ * Get the singleton MemoryService instance (created lazily)
1302
+ *
1303
+ * Note: Config is only applied on first call. Subsequent calls
1304
+ * return the existing instance regardless of config parameter.
1305
+ * Use createMemoryService() if you need a fresh instance with specific config.
1306
+ */
1307
+ export function getMemoryService(config?: Partial<MemoryServiceConfig>): MemoryService {
1308
+ if (!_serviceInstance) {
1309
+ _serviceInstance = new MemoryService(config)
1310
+ }
1311
+ return _serviceInstance
1312
+ }
1313
+
1314
+ /**
1315
+ * Reset the singleton instance (useful for testing)
1316
+ */
1317
+ export function resetMemoryService(): void {
1318
+ _serviceInstance = null
1319
+ }
1320
+
1321
+ /**
1322
+ * Create a new MemoryService instance (for testing or custom configs)
1323
+ */
1324
+ export function createMemoryService(
1325
+ config?: Partial<MemoryServiceConfig>,
1326
+ repository?: MemoryRepository
1327
+ ): MemoryService {
1328
+ return new MemoryService(config, repository)
1329
+ }
1330
+
1331
+ /**
1332
+ * Proxy-based lazy singleton for backwards compatibility
1333
+ */
1334
+ export const memoryService = new Proxy({} as MemoryService, {
1335
+ get(_, prop) {
1336
+ return getMemoryService()[prop as keyof MemoryService]
1337
+ },
1338
+ })