@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,438 @@
1
+ /**
2
+ * LLM Prompts for Memory Extraction and Relationship Detection
3
+ *
4
+ * Contains carefully crafted prompts with few-shot examples for accurate
5
+ * memory extraction and classification.
6
+ */
7
+
8
+ import type { MemoryType } from '../../types/index.js'
9
+
10
+ // ============================================================================
11
+ // Memory Extraction Prompts
12
+ // ============================================================================
13
+
14
+ /**
15
+ * System prompt for memory extraction
16
+ */
17
+ export const MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are an expert memory extraction system. Your task is to extract discrete, standalone facts, preferences, skills, and episodic memories from text content.
18
+
19
+ For each extracted memory, you must:
20
+ 1. Create a clear, standalone statement that makes sense without context
21
+ 2. Classify it into the correct type
22
+ 3. Assign a confidence score (0.0-1.0) based on clarity and reliability
23
+ 4. Extract relevant entities (people, places, organizations, dates)
24
+ 5. Identify key keywords
25
+
26
+ Memory Types:
27
+ - fact: Objective information, statements of truth, definitions
28
+ - event: Time-bound occurrences, meetings, experiences
29
+ - preference: Personal likes, dislikes, preferences, opinions
30
+ - skill: Abilities, capabilities, expertise, knowledge areas
31
+ - relationship: Interpersonal connections, social bonds
32
+ - context: Current situations, states, or ongoing activities
33
+ - note: General notes, reminders, todos
34
+
35
+ Guidelines:
36
+ - Each memory should be self-contained and understandable alone
37
+ - Be precise and avoid vague statements
38
+ - Include relevant context in the memory itself
39
+ - Higher confidence for explicit statements, lower for inferences
40
+ - Extract multiple memories from complex sentences`
41
+
42
+ /**
43
+ * Few-shot examples for memory extraction
44
+ */
45
+ export const MEMORY_EXTRACTION_EXAMPLES = `
46
+ Example 1:
47
+ Input: "I've been using TypeScript for 3 years now and really prefer it over plain JavaScript. Currently working on a React project for my company Acme Inc."
48
+
49
+ Output:
50
+ {
51
+ "memories": [
52
+ {
53
+ "content": "Has been using TypeScript for 3 years",
54
+ "type": "skill",
55
+ "confidence": 0.95,
56
+ "entities": [{"name": "TypeScript", "type": "concept"}],
57
+ "keywords": ["typescript", "programming", "experience"]
58
+ },
59
+ {
60
+ "content": "Prefers TypeScript over plain JavaScript for development",
61
+ "type": "preference",
62
+ "confidence": 0.90,
63
+ "entities": [{"name": "TypeScript", "type": "concept"}, {"name": "JavaScript", "type": "concept"}],
64
+ "keywords": ["typescript", "javascript", "preference"]
65
+ },
66
+ {
67
+ "content": "Currently working on a React project",
68
+ "type": "context",
69
+ "confidence": 0.85,
70
+ "entities": [{"name": "React", "type": "concept"}],
71
+ "keywords": ["react", "project", "current"]
72
+ },
73
+ {
74
+ "content": "Works for Acme Inc.",
75
+ "type": "relationship",
76
+ "confidence": 0.90,
77
+ "entities": [{"name": "Acme Inc.", "type": "organization"}],
78
+ "keywords": ["employment", "company", "work"]
79
+ }
80
+ ]
81
+ }
82
+
83
+ Example 2:
84
+ Input: "Note: Remember to update the API docs before the Friday release. The authentication endpoint was changed last week."
85
+
86
+ Output:
87
+ {
88
+ "memories": [
89
+ {
90
+ "content": "Need to update API docs before Friday release",
91
+ "type": "note",
92
+ "confidence": 0.85,
93
+ "entities": [],
94
+ "keywords": ["api", "documentation", "release", "friday"]
95
+ },
96
+ {
97
+ "content": "Authentication endpoint was modified recently",
98
+ "type": "event",
99
+ "confidence": 0.90,
100
+ "entities": [],
101
+ "keywords": ["authentication", "endpoint", "change", "api"]
102
+ }
103
+ ]
104
+ }
105
+
106
+ Example 3:
107
+ Input: "Dr. Sarah Chen, my mentor at Stanford, taught me that clean code is more important than clever code."
108
+
109
+ Output:
110
+ {
111
+ "memories": [
112
+ {
113
+ "content": "Dr. Sarah Chen is a mentor",
114
+ "type": "relationship",
115
+ "confidence": 0.95,
116
+ "entities": [{"name": "Dr. Sarah Chen", "type": "person"}, {"name": "Stanford", "type": "organization"}],
117
+ "keywords": ["mentor", "relationship"]
118
+ },
119
+ {
120
+ "content": "Believes clean code is more important than clever code",
121
+ "type": "preference",
122
+ "confidence": 0.85,
123
+ "entities": [],
124
+ "keywords": ["clean code", "programming", "philosophy"]
125
+ },
126
+ {
127
+ "content": "Has connection to Stanford",
128
+ "type": "relationship",
129
+ "confidence": 0.80,
130
+ "entities": [{"name": "Stanford", "type": "organization"}],
131
+ "keywords": ["stanford", "education"]
132
+ }
133
+ ]
134
+ }`
135
+
136
+ /**
137
+ * Generate the user prompt for memory extraction
138
+ */
139
+ export function generateExtractionPrompt(
140
+ text: string,
141
+ options?: {
142
+ containerTag?: string
143
+ context?: string
144
+ maxMemories?: number
145
+ minConfidence?: number
146
+ }
147
+ ): string {
148
+ let prompt = `Extract memories from the following text. Return a JSON object with a "memories" array.\n\n`
149
+
150
+ if (options?.containerTag) {
151
+ prompt += `Container/Category: ${options.containerTag}\n`
152
+ }
153
+
154
+ if (options?.context) {
155
+ prompt += `Additional Context: ${options.context}\n`
156
+ }
157
+
158
+ if (options?.maxMemories) {
159
+ prompt += `Maximum memories to extract: ${options.maxMemories}\n`
160
+ }
161
+
162
+ if (options?.minConfidence) {
163
+ prompt += `Minimum confidence threshold: ${options.minConfidence}\n`
164
+ }
165
+
166
+ prompt += `\nText to analyze:\n"""\n${text}\n"""\n\n`
167
+ prompt += `Respond with ONLY a valid JSON object in this exact format:
168
+ {
169
+ "memories": [
170
+ {
171
+ "content": "string - standalone statement",
172
+ "type": "fact|event|preference|skill|relationship|context|note",
173
+ "confidence": 0.0-1.0,
174
+ "entities": [{"name": "string", "type": "person|place|organization|date|concept|other"}],
175
+ "keywords": ["string"]
176
+ }
177
+ ]
178
+ }`
179
+
180
+ return prompt
181
+ }
182
+
183
+ // ============================================================================
184
+ // Relationship Detection Prompts
185
+ // ============================================================================
186
+
187
+ /**
188
+ * System prompt for relationship detection
189
+ */
190
+ export const RELATIONSHIP_DETECTION_SYSTEM_PROMPT = `You are an expert at detecting semantic relationships between pieces of information.
191
+
192
+ Given a NEW memory and a list of EXISTING memories, determine what relationships exist.
193
+
194
+ Relationship Types:
195
+ - updates: NEW contradicts or corrects OLD, making OLD outdated
196
+ - extends: NEW adds detail or elaboration to OLD without contradicting
197
+ - derives: NEW is a logical consequence or inference from OLD
198
+ - contradicts: NEW directly conflicts with OLD (both may be valid from different times)
199
+ - related: NEW is semantically similar or topically connected to OLD
200
+ - supersedes: NEW completely replaces OLD (OLD should be archived)
201
+
202
+ Guidelines:
203
+ - Only identify relationships with confidence >= 0.6
204
+ - "updates" and "supersedes" should mark the old memory for supersession
205
+ - "contradicts" does NOT mean the old memory should be removed (both may be valid)
206
+ - Consider temporal context when detecting updates
207
+ - Be conservative - prefer no relationship over a weak one`
208
+
209
+ /**
210
+ * Few-shot examples for relationship detection
211
+ */
212
+ export const RELATIONSHIP_DETECTION_EXAMPLES = `
213
+ Example 1:
214
+ NEW Memory: { "id": "new1", "content": "Uses Python 3.11 for all projects", "type": "preference" }
215
+ EXISTING Memories: [
216
+ { "id": "old1", "content": "Prefers Python 3.9", "type": "preference" },
217
+ { "id": "old2", "content": "Expert in Python programming", "type": "skill" }
218
+ ]
219
+
220
+ Output:
221
+ {
222
+ "relationships": [
223
+ {
224
+ "sourceMemoryId": "new1",
225
+ "targetMemoryId": "old1",
226
+ "type": "updates",
227
+ "confidence": 0.90,
228
+ "reason": "New version preference supersedes old version preference"
229
+ },
230
+ {
231
+ "sourceMemoryId": "new1",
232
+ "targetMemoryId": "old2",
233
+ "type": "related",
234
+ "confidence": 0.75,
235
+ "reason": "Both relate to Python programming"
236
+ }
237
+ ],
238
+ "supersededMemoryIds": ["old1"]
239
+ }
240
+
241
+ Example 2:
242
+ NEW Memory: { "id": "new2", "content": "The API now supports batch operations", "type": "fact" }
243
+ EXISTING Memories: [
244
+ { "id": "old3", "content": "The API only supports single-item operations", "type": "fact" },
245
+ { "id": "old4", "content": "Working on adding batch support to the API", "type": "context" }
246
+ ]
247
+
248
+ Output:
249
+ {
250
+ "relationships": [
251
+ {
252
+ "sourceMemoryId": "new2",
253
+ "targetMemoryId": "old3",
254
+ "type": "supersedes",
255
+ "confidence": 0.95,
256
+ "reason": "New capability statement makes old limitation statement obsolete"
257
+ },
258
+ {
259
+ "sourceMemoryId": "new2",
260
+ "targetMemoryId": "old4",
261
+ "type": "derives",
262
+ "confidence": 0.85,
263
+ "reason": "Batch support being complete is a result of the work mentioned"
264
+ }
265
+ ],
266
+ "supersededMemoryIds": ["old3", "old4"]
267
+ }`
268
+
269
+ /**
270
+ * Generate the user prompt for relationship detection
271
+ */
272
+ export function generateRelationshipPrompt(
273
+ newMemory: { id: string; content: string; type: MemoryType },
274
+ existingMemories: Array<{ id: string; content: string; type: MemoryType }>,
275
+ options?: {
276
+ maxRelationships?: number
277
+ minConfidence?: number
278
+ }
279
+ ): string {
280
+ let prompt = `Analyze the relationship between the NEW memory and EXISTING memories.\n\n`
281
+
282
+ prompt += `NEW Memory:\n${JSON.stringify(newMemory, null, 2)}\n\n`
283
+ prompt += `EXISTING Memories:\n${JSON.stringify(existingMemories, null, 2)}\n\n`
284
+
285
+ if (options?.minConfidence) {
286
+ prompt += `Only include relationships with confidence >= ${options.minConfidence}\n`
287
+ }
288
+
289
+ if (options?.maxRelationships) {
290
+ prompt += `Return at most ${options.maxRelationships} relationships\n`
291
+ }
292
+
293
+ prompt += `\nRespond with ONLY a valid JSON object in this exact format:
294
+ {
295
+ "relationships": [
296
+ {
297
+ "sourceMemoryId": "string - always the NEW memory id",
298
+ "targetMemoryId": "string - an EXISTING memory id",
299
+ "type": "updates|extends|derives|contradicts|related|supersedes",
300
+ "confidence": 0.0-1.0,
301
+ "reason": "string - brief explanation"
302
+ }
303
+ ],
304
+ "supersededMemoryIds": ["string - ids of memories that should be marked as outdated"]
305
+ }`
306
+
307
+ return prompt
308
+ }
309
+
310
+ // ============================================================================
311
+ // Response Parsing
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Normalize LLM JSON responses by stripping markdown fences and common wrappers.
316
+ */
317
+ export function normalizeJsonResponse(response: string): string {
318
+ const trimmed = response.trim()
319
+
320
+ // Prefer fenced JSON blocks when present (allows explanatory prose around JSON).
321
+ const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)
322
+ if (fencedMatch?.[1]) {
323
+ return fencedMatch[1].trim()
324
+ }
325
+
326
+ // Fallback: extract the outermost JSON object if surrounded by extra text.
327
+ const firstBrace = trimmed.indexOf('{')
328
+ const lastBrace = trimmed.lastIndexOf('}')
329
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
330
+ return trimmed.slice(firstBrace, lastBrace + 1).trim()
331
+ }
332
+
333
+ return trimmed
334
+ }
335
+
336
+ /**
337
+ * Parse and validate extraction response from LLM
338
+ */
339
+ export function parseExtractionResponse(response: string): {
340
+ memories: Array<{
341
+ content: string
342
+ type: MemoryType
343
+ confidence: number
344
+ entities: Array<{ name: string; type: string }>
345
+ keywords: string[]
346
+ }>
347
+ } {
348
+ const cleaned = normalizeJsonResponse(response)
349
+
350
+ try {
351
+ const parsed = JSON.parse(cleaned)
352
+
353
+ if (!parsed.memories || !Array.isArray(parsed.memories)) {
354
+ throw new Error('Response missing memories array')
355
+ }
356
+
357
+ // Validate and clean each memory
358
+ const validTypes: MemoryType[] = ['fact', 'event', 'preference', 'skill', 'relationship', 'context', 'note']
359
+
360
+ const memories = parsed.memories
361
+ .filter((m: unknown) => {
362
+ if (!m || typeof m !== 'object') return false
363
+ const mem = m as Record<string, unknown>
364
+ return (
365
+ typeof mem.content === 'string' &&
366
+ mem.content.length > 0 &&
367
+ typeof mem.type === 'string' &&
368
+ validTypes.includes(mem.type as MemoryType)
369
+ )
370
+ })
371
+ .map((m: Record<string, unknown>) => ({
372
+ content: String(m.content).trim(),
373
+ type: m.type as MemoryType,
374
+ confidence: typeof m.confidence === 'number' ? Math.max(0, Math.min(1, m.confidence)) : 0.5,
375
+ entities: Array.isArray(m.entities)
376
+ ? m.entities.filter(
377
+ (e: unknown) => e && typeof e === 'object' && 'name' in (e as object) && 'type' in (e as object)
378
+ )
379
+ : [],
380
+ keywords: Array.isArray(m.keywords) ? m.keywords.filter((k: unknown) => typeof k === 'string') : [],
381
+ }))
382
+
383
+ return { memories }
384
+ } catch (error) {
385
+ throw new Error(`Failed to parse extraction response: ${error instanceof Error ? error.message : String(error)}`)
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Parse and validate relationship response from LLM
391
+ */
392
+ export function parseRelationshipResponse(response: string): {
393
+ relationships: Array<{
394
+ sourceMemoryId: string
395
+ targetMemoryId: string
396
+ type: string
397
+ confidence: number
398
+ reason: string
399
+ }>
400
+ supersededMemoryIds: string[]
401
+ } {
402
+ const cleaned = normalizeJsonResponse(response)
403
+
404
+ try {
405
+ const parsed = JSON.parse(cleaned)
406
+
407
+ const validTypes = ['updates', 'extends', 'derives', 'contradicts', 'related', 'supersedes']
408
+
409
+ const relationships = Array.isArray(parsed.relationships)
410
+ ? parsed.relationships
411
+ .filter((r: unknown) => {
412
+ if (!r || typeof r !== 'object') return false
413
+ const rel = r as Record<string, unknown>
414
+ return (
415
+ typeof rel.sourceMemoryId === 'string' &&
416
+ typeof rel.targetMemoryId === 'string' &&
417
+ typeof rel.type === 'string' &&
418
+ validTypes.includes(rel.type)
419
+ )
420
+ })
421
+ .map((r: Record<string, unknown>) => ({
422
+ sourceMemoryId: String(r.sourceMemoryId),
423
+ targetMemoryId: String(r.targetMemoryId),
424
+ type: String(r.type),
425
+ confidence: typeof r.confidence === 'number' ? Math.max(0, Math.min(1, r.confidence)) : 0.5,
426
+ reason: typeof r.reason === 'string' ? r.reason : 'No reason provided',
427
+ }))
428
+ : []
429
+
430
+ const supersededMemoryIds = Array.isArray(parsed.supersededMemoryIds)
431
+ ? parsed.supersededMemoryIds.filter((id: unknown) => typeof id === 'string')
432
+ : []
433
+
434
+ return { relationships, supersededMemoryIds }
435
+ } catch (error) {
436
+ throw new Error(`Failed to parse relationship response: ${error instanceof Error ? error.message : String(error)}`)
437
+ }
438
+ }