@lota-sdk/core 0.1.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.
Files changed (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,974 @@
1
+ import { BoundQuery, eq, inside } from 'surrealdb'
2
+
3
+ import { aiLogger } from '../config/logger'
4
+ import { DEFAULT_MEMORY_SEARCH_LIMIT } from '../config/search'
5
+ import { createDefaultEmbeddings } from '../embeddings/provider'
6
+ import { memoryQueryBuilder } from './memory-query-builder'
7
+ import type { RelationCounts } from './memory-store.helpers'
8
+ import { hashContent, mapRowToMemoryRecord, processGraphAwareRows } from './memory-store.helpers'
9
+ import type { BasicSearchRow, SurrealMemoryRow } from './memory-store.rows'
10
+ import type { LinearNormalization, MemoryEvent, MemoryRecord, MemorySearchResult, RelationType } from './memory-types'
11
+ import { ensureRecordId, recordIdToString } from './record-id'
12
+ import type { RecordIdInput, RecordIdRef } from './record-id'
13
+ import { databaseService } from './service'
14
+ import { TABLES } from './tables'
15
+
16
+ const MEMORY_TABLE = TABLES.MEMORY
17
+ const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
18
+ const MEMORY_RELATION_TABLE = TABLES.MEMORY_RELATION
19
+ const EMBEDDING_CACHE_TTL_MS = 5 * 60 * 1000
20
+ const EMBEDDING_CACHE_MAX_ENTRIES = 64
21
+ const MIN_RELEVANCE_SCORE = 0.25
22
+ const TOUCH_MEMORIES_MAX_ATTEMPTS = 4
23
+ const TOUCH_MEMORIES_RETRY_BASE_DELAY_MS = 25
24
+ const TOUCH_MEMORIES_RETRY_JITTER_MS = 20
25
+
26
+ interface EmbeddingClient {
27
+ embedQuery(text: string): Promise<number[]>
28
+ }
29
+
30
+ export class SurrealMemoryStore {
31
+ private embeddingCache = new Map<string, { embedding: number[]; ts: number }>()
32
+ private embeddingInFlight = new Map<string, Promise<number[]>>()
33
+
34
+ constructor(private embeddings: EmbeddingClient) {}
35
+
36
+ private tokenizeQuery(query: string): string[] {
37
+ return query
38
+ .toLowerCase()
39
+ .split(/\s+/)
40
+ .map((token) => token.trim())
41
+ .filter((token) => token.length >= 3)
42
+ .slice(0, 12)
43
+ }
44
+
45
+ private scoreTextMatch(content: string, tokens: string[], fullQuery: string): number {
46
+ if (!content) return 0
47
+ const haystack = content.toLowerCase()
48
+ let score = 0
49
+
50
+ const normalizedQuery = fullQuery.trim().toLowerCase()
51
+ if (normalizedQuery.length >= 3 && haystack.includes(normalizedQuery)) {
52
+ score += 3
53
+ }
54
+
55
+ for (const token of tokens) {
56
+ if (haystack.includes(token)) score += 1
57
+ }
58
+
59
+ return score
60
+ }
61
+
62
+ private async listRecentBasic(options: {
63
+ scopeId: string
64
+ limit: number
65
+ memoryType?: MemoryRecord['memoryType']
66
+ }): Promise<BasicSearchRow[]> {
67
+ const typeFilter = options.memoryType ? 'AND memoryType = $memoryType' : ''
68
+ const sql = `
69
+ SELECT id, content, metadata, createdAt
70
+ FROM ${MEMORY_TABLE}
71
+ WHERE scopeId = $scopeId ${typeFilter}
72
+ AND archivedAt IS NONE AND (validUntil IS NONE OR validUntil > time::now())
73
+ ORDER BY createdAt DESC
74
+ LIMIT $limit
75
+ `
76
+
77
+ return await databaseService.query<BasicSearchRow>(
78
+ new BoundQuery(sql, { scopeId: options.scopeId, memoryType: options.memoryType, limit: options.limit }),
79
+ )
80
+ }
81
+
82
+ async listTopMemories(options: {
83
+ scopeId: string
84
+ limit: number
85
+ memoryType?: MemoryRecord['memoryType']
86
+ durability?: MemoryRecord['durability']
87
+ minImportance?: number
88
+ }): Promise<MemoryRecord[]> {
89
+ const typeFilter = options.memoryType ? 'AND memoryType = $memoryType' : ''
90
+ const durabilityFilter = options.durability ? 'AND durability = $durability' : ''
91
+ const importanceFilter = typeof options.minImportance === 'number' ? 'AND importance >= $minImportance' : ''
92
+ const sql = `
93
+ SELECT *
94
+ FROM ${MEMORY_TABLE}
95
+ WHERE scopeId = $scopeId ${typeFilter} ${durabilityFilter} ${importanceFilter}
96
+ AND archivedAt IS NONE AND (validUntil IS NONE OR validUntil > time::now())
97
+ ORDER BY importance DESC, accessCount DESC, lastAccessedAt DESC, createdAt DESC
98
+ LIMIT $limit
99
+ `
100
+
101
+ const rows = await databaseService.query<SurrealMemoryRow>(
102
+ new BoundQuery(sql, {
103
+ scopeId: options.scopeId,
104
+ memoryType: options.memoryType,
105
+ durability: options.durability,
106
+ minImportance: options.minImportance,
107
+ limit: options.limit,
108
+ }),
109
+ )
110
+
111
+ return rows.map((row) => mapRowToMemoryRecord(row))
112
+ }
113
+
114
+ private async vectorSearchWithEmbedding(options: {
115
+ embedding: number[]
116
+ scopeId: string
117
+ limit: number
118
+ memoryType?: MemoryRecord['memoryType']
119
+ fastMode?: boolean
120
+ }): Promise<MemorySearchResult[]> {
121
+ const { sql, bindVars } = memoryQueryBuilder.buildVectorSearch({
122
+ embedding: options.embedding,
123
+ scopeId: options.scopeId,
124
+ limit: options.limit,
125
+ memoryType: options.memoryType,
126
+ })
127
+
128
+ const results = await this.queryFinalStatement<BasicSearchRow & { distance: number }>(sql, bindVars)
129
+ if (results.length === 0) return []
130
+ if (options.fastMode) {
131
+ return this.mapFastRows(results, options.limit, (row) => 1 / (1 + row.distance))
132
+ }
133
+
134
+ const memoryIds = results.map((row) => row.id)
135
+ const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
136
+
137
+ const processed = processGraphAwareRows(
138
+ results,
139
+ relationCounts,
140
+ options.limit,
141
+ (row) => 1 / (1 + row.distance),
142
+ { support: 0.1, contradict: 0.2 },
143
+ MIN_RELEVANCE_SCORE,
144
+ )
145
+
146
+ this.touchMemories(processed.map((row) => row.id))
147
+ return processed
148
+ }
149
+
150
+ private mapFastRows<T extends BasicSearchRow>(
151
+ rows: T[],
152
+ limit: number,
153
+ scoreResolver: (row: T, index: number) => number,
154
+ ): MemorySearchResult[] {
155
+ return rows
156
+ .slice(0, limit)
157
+ .map((row, index) => ({
158
+ id: recordIdToString(row.id, TABLES.MEMORY),
159
+ content: row.content,
160
+ score: scoreResolver(row, index),
161
+ metadata: row.metadata,
162
+ }))
163
+ }
164
+
165
+ private getEmbeddingCacheKey(content: string): string {
166
+ return content.trim().toLowerCase()
167
+ }
168
+
169
+ private pruneEmbeddingCache(now: number): void {
170
+ for (const [cacheKey, entry] of this.embeddingCache.entries()) {
171
+ if (now - entry.ts > EMBEDDING_CACHE_TTL_MS) {
172
+ this.embeddingCache.delete(cacheKey)
173
+ }
174
+ }
175
+
176
+ while (this.embeddingCache.size > EMBEDDING_CACHE_MAX_ENTRIES) {
177
+ const oldest = this.embeddingCache.keys().next().value
178
+ if (!oldest) break
179
+ this.embeddingCache.delete(oldest)
180
+ }
181
+ }
182
+
183
+ private async generateEmbedding(content: string): Promise<number[]> {
184
+ const normalized = content.trim()
185
+ if (!normalized) return []
186
+
187
+ const cacheKey = this.getEmbeddingCacheKey(normalized)
188
+ const now = Date.now()
189
+ this.pruneEmbeddingCache(now)
190
+
191
+ const cached = this.embeddingCache.get(cacheKey)
192
+ if (cached && now - cached.ts <= EMBEDDING_CACHE_TTL_MS) {
193
+ return cached.embedding
194
+ }
195
+
196
+ const inFlight = this.embeddingInFlight.get(cacheKey)
197
+ if (inFlight) {
198
+ return await inFlight
199
+ }
200
+
201
+ const request = this.embeddings
202
+ .embedQuery(normalized)
203
+ .then((embedding) => {
204
+ this.embeddingCache.set(cacheKey, { embedding, ts: Date.now() })
205
+ this.pruneEmbeddingCache(Date.now())
206
+ return embedding
207
+ })
208
+ .finally(() => {
209
+ this.embeddingInFlight.delete(cacheKey)
210
+ })
211
+
212
+ this.embeddingInFlight.set(cacheKey, request)
213
+ return await request
214
+ }
215
+
216
+ async warmEmbedding(content: string): Promise<void> {
217
+ await this.generateEmbedding(content)
218
+ }
219
+
220
+ private async fetchRelationCountsBatch(memoryIds: RecordIdInput[]): Promise<Map<string, RelationCounts>> {
221
+ if (memoryIds.length === 0) {
222
+ return new Map()
223
+ }
224
+
225
+ const memoryRefs = memoryIds.map((id) => ensureRecordId(id, TABLES.MEMORY))
226
+
227
+ const sql = `
228
+ SELECT
229
+ id,
230
+ count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']) AS supersedeCount,
231
+ count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'contradicts']) AS contradictCount,
232
+ count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supports']) AS supportCount,
233
+ <-${MEMORY_RELATION_TABLE}[WHERE relationType = 'contradicts']<-${MEMORY_TABLE}.content AS contradictionTexts
234
+ FROM ${MEMORY_TABLE}
235
+ WHERE id IN $memoryIds
236
+ `
237
+
238
+ const results = await databaseService.query<{
239
+ id: RecordIdInput
240
+ supersedeCount: number
241
+ contradictCount: number
242
+ supportCount: number
243
+ contradictionTexts: string[] | null
244
+ }>(new BoundQuery(sql, { memoryIds: memoryRefs }))
245
+
246
+ const countsMap = new Map<string, RelationCounts>()
247
+ for (const row of results) {
248
+ const rawTexts = row.contradictionTexts ?? []
249
+ const contradictions = rawTexts.filter((text): text is string => typeof text === 'string' && text.length > 0)
250
+ countsMap.set(recordIdToString(row.id, TABLES.MEMORY), {
251
+ supersedeCount: row.supersedeCount,
252
+ contradictCount: row.contradictCount,
253
+ supportCount: row.supportCount,
254
+ contradictions,
255
+ })
256
+ }
257
+
258
+ return countsMap
259
+ }
260
+
261
+ private touchMemories(memoryIds: string[]): void {
262
+ if (memoryIds.length === 0) return
263
+ const uniqueIds = [...new Set(memoryIds)]
264
+ const memoryRefs = uniqueIds.map((id) => ensureRecordId(id, TABLES.MEMORY))
265
+ const sql = `
266
+ UPDATE ${MEMORY_TABLE}
267
+ SET accessCount += 1, lastAccessedAt = time::now()
268
+ WHERE id IN $memoryIds
269
+ `
270
+ const query = new BoundQuery(sql, { memoryIds: memoryRefs })
271
+
272
+ void this.runTouchMemoriesWithRetry(query)
273
+ }
274
+
275
+ private async runTouchMemoriesWithRetry(query: BoundQuery): Promise<void> {
276
+ for (let attempt = 1; attempt <= TOUCH_MEMORIES_MAX_ATTEMPTS; attempt += 1) {
277
+ try {
278
+ await databaseService.query(query)
279
+ return
280
+ } catch (error) {
281
+ const retriable = this.isRetriableTransactionConflict(error)
282
+ const hasMoreAttempts = attempt < TOUCH_MEMORIES_MAX_ATTEMPTS
283
+ if (!retriable || !hasMoreAttempts) {
284
+ aiLogger.warn`Failed to update memory access counters after ${attempt} attempt(s): ${error}`
285
+ return
286
+ }
287
+
288
+ const backoffMs =
289
+ TOUCH_MEMORIES_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) +
290
+ Math.floor(Math.random() * TOUCH_MEMORIES_RETRY_JITTER_MS)
291
+ await Bun.sleep(backoffMs)
292
+ }
293
+ }
294
+ }
295
+
296
+ private isRetriableTransactionConflict(error: unknown): boolean {
297
+ if (!(error instanceof Error)) return false
298
+ const message = error.message.toLowerCase()
299
+ return message.includes('transaction conflict') || message.includes('this transaction can be retried')
300
+ }
301
+
302
+ private async queryFinalStatement<T>(sql: string, bindVars: Record<string, unknown>): Promise<T[]> {
303
+ const statements = await databaseService.queryAll<unknown>(new BoundQuery(sql, bindVars))
304
+ const finalStatement = statements.at(-1)
305
+ return Array.isArray(finalStatement) ? (finalStatement as T[]) : []
306
+ }
307
+
308
+ private async fallbackWeightedSearch(
309
+ query: string,
310
+ tokens: string[],
311
+ options: {
312
+ embedding: number[]
313
+ scopeId: string
314
+ limit: number
315
+ memoryType?: MemoryRecord['memoryType']
316
+ reason: string
317
+ fastMode?: boolean
318
+ },
319
+ ): Promise<MemorySearchResult[]> {
320
+ aiLogger.debug`Weighted hybrid search fallback to vector/recent (scopeId: ${options.scopeId}, reason: ${options.reason})`
321
+ const vectorResults = await this.vectorSearchWithEmbedding({
322
+ embedding: options.embedding,
323
+ scopeId: options.scopeId,
324
+ limit: options.limit,
325
+ memoryType: options.memoryType,
326
+ fastMode: options.fastMode,
327
+ })
328
+
329
+ if (vectorResults.length > 0) {
330
+ return vectorResults
331
+ }
332
+
333
+ const recentLimit = Math.max(50, options.limit * 10)
334
+ const recent = await this.listRecentBasic({
335
+ scopeId: options.scopeId,
336
+ limit: recentLimit,
337
+ memoryType: options.memoryType,
338
+ })
339
+
340
+ if (recent.length === 0) {
341
+ return []
342
+ }
343
+
344
+ const scoredRows = recent
345
+ .map((row, index) => ({ ...row, index, textScore: this.scoreTextMatch(row.content, tokens, query) }))
346
+ .sort((a, b) => {
347
+ if (b.textScore !== a.textScore) return b.textScore - a.textScore
348
+ return a.index - b.index
349
+ })
350
+ .slice(0, Math.max(options.limit * 4, 50))
351
+
352
+ if (options.fastMode) {
353
+ return this.mapFastRows(scoredRows, options.limit, (row) => row.textScore)
354
+ }
355
+
356
+ const recentIds = scoredRows.map((row) => row.id)
357
+ const recentRelationCounts = await this.fetchRelationCountsBatch(recentIds)
358
+ const processed = processGraphAwareRows(
359
+ scoredRows,
360
+ recentRelationCounts,
361
+ options.limit,
362
+ (row) => row.textScore,
363
+ { support: 0.05, contradict: 0.1 },
364
+ MIN_RELEVANCE_SCORE,
365
+ )
366
+
367
+ this.touchMemories(processed.map((row) => row.id))
368
+ return processed
369
+ }
370
+
371
+ private static WRITE_DEDUP_THRESHOLD = 0.9
372
+
373
+ async insert(
374
+ content: string,
375
+ scopeId: string,
376
+ memoryType: MemoryRecord['memoryType'],
377
+ metadata: Record<string, unknown> = {},
378
+ importance: number = 1,
379
+ durability: MemoryRecord['durability'] = 'standard',
380
+ ): Promise<string> {
381
+ const hash = hashContent(content, scopeId, memoryType)
382
+ const embedding = await this.generateEmbedding(content)
383
+
384
+ importance = Math.max(0, Math.min(1, importance))
385
+
386
+ const nearDup = await this.findNearDuplicate(embedding, scopeId, content)
387
+ if (nearDup) {
388
+ const mergedImportance = Math.max(0, Math.min(1, Math.max(nearDup.importance, importance)))
389
+ const keepNew = content.length >= nearDup.content.length
390
+ const winnerContent = keepNew ? content : nearDup.content
391
+ await this.update(nearDup.id, winnerContent, { importance: mergedImportance })
392
+ aiLogger.debug`Write-time dedup: merged into existing memory ${nearDup.id} (similarity: ${nearDup.similarity.toFixed(3)})`
393
+ return nearDup.id
394
+ }
395
+
396
+ const result = await databaseService.insert<{ id: RecordIdInput }>(MEMORY_TABLE, {
397
+ content,
398
+ embedding,
399
+ hash,
400
+ scopeId,
401
+ memoryType,
402
+ metadata,
403
+ importance,
404
+ durability,
405
+ })
406
+
407
+ const id = result[0]?.id ? recordIdToString(result[0].id, TABLES.MEMORY) : ''
408
+
409
+ await this.recordHistory(id, null, content, 'ADD')
410
+
411
+ return id
412
+ }
413
+
414
+ async getByHash(hash: string): Promise<MemoryRecord | null> {
415
+ const rows = await databaseService.query<SurrealMemoryRow>(
416
+ new BoundQuery(
417
+ `
418
+ SELECT *
419
+ FROM ${MEMORY_TABLE}
420
+ WHERE hash = $hash
421
+ LIMIT 1
422
+ `,
423
+ { hash },
424
+ ),
425
+ )
426
+
427
+ const row = rows.at(0)
428
+ return row ? mapRowToMemoryRecord(row) : null
429
+ }
430
+
431
+ private async findNearDuplicate(
432
+ embedding: number[],
433
+ scopeId: string,
434
+ content: string,
435
+ ): Promise<{ id: string; content: string; importance: number; similarity: number } | null> {
436
+ const candidateLimit = 12
437
+ const sql = `
438
+ LET $candidateRows = (
439
+ SELECT
440
+ id,
441
+ vector::distance::knn() AS distance
442
+ FROM ${MEMORY_TABLE}
443
+ WHERE scopeId = $scopeId
444
+ AND archivedAt IS NONE
445
+ AND embedding <|${candidateLimit}|> $embedding
446
+ ORDER BY distance ASC
447
+ LIMIT ${candidateLimit}
448
+ );
449
+
450
+ SELECT
451
+ id,
452
+ content,
453
+ importance,
454
+ vector::similarity::cosine(embedding, $embedding) AS similarity
455
+ FROM ${MEMORY_TABLE}
456
+ WHERE id IN $candidateRows.id
457
+ ORDER BY similarity DESC
458
+ LIMIT ${candidateLimit}
459
+ `
460
+
461
+ const neighborRows = await this.queryFinalStatement<{
462
+ id: RecordIdInput
463
+ content: string
464
+ importance: number
465
+ similarity: number
466
+ }>(sql, { scopeId, embedding })
467
+ const neighbors = neighborRows.map((row) => ({ ...row, id: recordIdToString(row.id, TABLES.MEMORY) }))
468
+
469
+ for (const neighbor of neighbors) {
470
+ if (neighbor.similarity < SurrealMemoryStore.WRITE_DEDUP_THRESHOLD) break
471
+ const [shorter, longer] =
472
+ content.length <= neighbor.content.length ? [content, neighbor.content] : [neighbor.content, content]
473
+ const a = shorter.toLowerCase().trim()
474
+ const b = longer.toLowerCase().trim()
475
+ if (b.includes(a)) return neighbor
476
+ const aWords = new Set(a.split(/\s+/))
477
+ const bWords = new Set(b.split(/\s+/))
478
+ let overlap = 0
479
+ for (const word of aWords) {
480
+ if (bWords.has(word)) overlap++
481
+ }
482
+ if (aWords.size > 0 && overlap / aWords.size >= 0.8) return neighbor
483
+ }
484
+
485
+ return null
486
+ }
487
+
488
+ async search(
489
+ query: string,
490
+ scopeId: string,
491
+ limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
492
+ memoryType?: MemoryRecord['memoryType'],
493
+ ): Promise<MemorySearchResult[]> {
494
+ aiLogger.debug`Memory store search (scopeId: ${scopeId}, memoryType: ${memoryType}, limit: ${limit})`
495
+ const queryEmbedding = await this.generateEmbedding(query)
496
+
497
+ const { sql, bindVars } = memoryQueryBuilder.buildVectorSearch({
498
+ embedding: queryEmbedding,
499
+ scopeId,
500
+ limit,
501
+ memoryType,
502
+ })
503
+
504
+ const results = await this.queryFinalStatement<BasicSearchRow & { distance: number }>(sql, bindVars)
505
+
506
+ aiLogger.debug`Memory store search raw results: ${results.length} rows found`
507
+
508
+ const memoryIds = results.map((row) => row.id)
509
+ const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
510
+
511
+ const processed = processGraphAwareRows(
512
+ results,
513
+ relationCounts,
514
+ limit,
515
+ (row) => 1 / (1 + row.distance),
516
+ { support: 0.1, contradict: 0.2 },
517
+ MIN_RELEVANCE_SCORE,
518
+ )
519
+
520
+ aiLogger.debug`Memory store search final results: ${processed.length} memories after filtering`
521
+ this.touchMemories(processed.map((row) => row.id))
522
+ return processed
523
+ }
524
+
525
+ async hybridSearch(
526
+ query: string,
527
+ scopeId: string,
528
+ limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
529
+ memoryType?: MemoryRecord['memoryType'],
530
+ ): Promise<MemorySearchResult[]> {
531
+ const queryEmbedding = await this.generateEmbedding(query)
532
+
533
+ const { sql, bindVars } = memoryQueryBuilder.buildHybridSearch({
534
+ query,
535
+ embedding: queryEmbedding,
536
+ scopeId,
537
+ limit,
538
+ memoryType,
539
+ })
540
+
541
+ type RrfRow = BasicSearchRow & { rrfScore: number }
542
+
543
+ const results = await this.queryFinalStatement<RrfRow>(sql, bindVars)
544
+
545
+ const memoryIds = results.map((row) => row.id)
546
+ const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
547
+
548
+ const processed = processGraphAwareRows(
549
+ results,
550
+ relationCounts,
551
+ limit,
552
+ (row) => row.rrfScore,
553
+ { support: 0.05, contradict: 0.1 },
554
+ MIN_RELEVANCE_SCORE,
555
+ )
556
+
557
+ this.touchMemories(processed.map((row) => row.id))
558
+ return processed
559
+ }
560
+
561
+ async hybridSearchWeighted(
562
+ query: string,
563
+ options: {
564
+ scopeId: string
565
+ limit: number
566
+ memoryType?: MemoryRecord['memoryType']
567
+ weights?: [number, number]
568
+ normalization?: LinearNormalization
569
+ fastMode?: boolean
570
+ },
571
+ ): Promise<MemorySearchResult[]> {
572
+ const queryEmbedding = await this.generateEmbedding(query)
573
+
574
+ const tokens = this.tokenizeQuery(query)
575
+ if (tokens.length === 0) {
576
+ aiLogger.debug`Skipping hybrid search (no valid tokens). Using vector search only.`
577
+ return this.vectorSearchWithEmbedding({
578
+ embedding: queryEmbedding,
579
+ scopeId: options.scopeId,
580
+ limit: options.limit,
581
+ memoryType: options.memoryType,
582
+ fastMode: options.fastMode,
583
+ })
584
+ }
585
+
586
+ const tokenQuery = tokens.join(' ')
587
+ const fullTextQuery = tokenQuery.length > 0 ? tokenQuery : query
588
+
589
+ const weights = options.weights ?? [2, 1]
590
+ const normalization = options.normalization ?? 'minmax'
591
+
592
+ const { sql, bindVars } = memoryQueryBuilder.buildLinearSearch({
593
+ query: fullTextQuery,
594
+ embedding: queryEmbedding,
595
+ scopeId: options.scopeId,
596
+ limit: options.limit,
597
+ memoryType: options.memoryType,
598
+ weights,
599
+ normalization,
600
+ })
601
+
602
+ type LinearRow = BasicSearchRow & { linearScore: number }
603
+
604
+ const results = await this.queryFinalStatement<LinearRow>(sql, bindVars)
605
+
606
+ if (results.length === 0) {
607
+ aiLogger.debug`Weighted hybrid search returned 0 raw results (scopeId: ${options.scopeId})`
608
+ return this.fallbackWeightedSearch(query, tokens, {
609
+ embedding: queryEmbedding,
610
+ scopeId: options.scopeId,
611
+ limit: options.limit,
612
+ memoryType: options.memoryType,
613
+ reason: 'no_raw_results',
614
+ fastMode: options.fastMode,
615
+ })
616
+ }
617
+
618
+ if (options.fastMode) {
619
+ const fastResults = this.mapFastRows(results, options.limit, (row) => row.linearScore)
620
+ return fastResults
621
+ }
622
+
623
+ const memoryIds = results.map((row) => row.id)
624
+ const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
625
+
626
+ const processed = processGraphAwareRows(
627
+ results,
628
+ relationCounts,
629
+ options.limit,
630
+ (row) => row.linearScore,
631
+ { support: 0.05, contradict: 0.1 },
632
+ MIN_RELEVANCE_SCORE,
633
+ )
634
+
635
+ if (processed.length === 0) {
636
+ aiLogger.debug`Weighted hybrid search candidates were fully filtered (scopeId: ${options.scopeId}). Falling back to vector/recent.`
637
+ return this.fallbackWeightedSearch(query, tokens, {
638
+ embedding: queryEmbedding,
639
+ scopeId: options.scopeId,
640
+ limit: options.limit,
641
+ memoryType: options.memoryType,
642
+ reason: 'filtered_to_zero',
643
+ fastMode: options.fastMode,
644
+ })
645
+ }
646
+
647
+ aiLogger.info`[SUCCESS_WEIGHTED_SEARCH] Weighted hybrid search succeeded (scopeId: ${options.scopeId}, rawResults: ${results.length}, returned: ${processed.length}, weights: ${weights.join(',')}, normalization: ${normalization})`
648
+ this.touchMemories(processed.map((row) => row.id))
649
+ return processed
650
+ }
651
+
652
+ async get(id: string): Promise<MemoryRecord | null> {
653
+ const sql = `SELECT * FROM ${MEMORY_TABLE} WHERE id = $id`
654
+ const results = await databaseService.query<SurrealMemoryRow>(
655
+ new BoundQuery(sql, { id: ensureRecordId(id, TABLES.MEMORY) }),
656
+ )
657
+
658
+ const row = results.at(0)
659
+ if (!row) return null
660
+ return mapRowToMemoryRecord(row)
661
+ }
662
+
663
+ async update(
664
+ id: string,
665
+ newContent: string,
666
+ options?: { importance?: number; durability?: MemoryRecord['durability']; metadata?: Record<string, unknown> },
667
+ ): Promise<void> {
668
+ const existing = await this.get(id)
669
+ if (!existing) return
670
+
671
+ const newHash = hashContent(newContent, existing.scopeId, existing.memoryType)
672
+ const newEmbedding = await this.generateEmbedding(newContent)
673
+
674
+ const importance =
675
+ typeof options?.importance === 'number'
676
+ ? Math.max(existing.importance, Math.max(0, Math.min(1, options.importance)))
677
+ : undefined
678
+
679
+ const durability = options?.durability
680
+ const metadata = options?.metadata ? { ...existing.metadata, ...options.metadata } : undefined
681
+
682
+ const updatePayload: Record<string, unknown> = {
683
+ content: newContent,
684
+ embedding: newEmbedding,
685
+ hash: newHash,
686
+ updatedAt: new Date(),
687
+ }
688
+ if (importance !== undefined) {
689
+ updatePayload.importance = importance
690
+ }
691
+ if (durability !== undefined) {
692
+ updatePayload.durability = durability
693
+ }
694
+ if (metadata !== undefined) {
695
+ updatePayload.metadata = metadata
696
+ }
697
+
698
+ await databaseService.updateWhere(MEMORY_TABLE, eq('id', ensureRecordId(id, TABLES.MEMORY)), updatePayload)
699
+
700
+ await this.recordHistory(id, existing.content, newContent, 'UPDATE')
701
+ }
702
+
703
+ async delete(id: string): Promise<void> {
704
+ const existing = await this.get(id)
705
+ if (!existing) return
706
+
707
+ await databaseService.deleteById(MEMORY_TABLE, id)
708
+
709
+ await this.recordHistory(id, existing.content, null, 'DELETE')
710
+ }
711
+
712
+ async list(scopeId: string, memoryType?: MemoryRecord['memoryType']): Promise<MemoryRecord[]> {
713
+ const typeFilter = memoryType ? 'AND memoryType = $memoryType' : ''
714
+ const sql = `
715
+ SELECT * FROM ${MEMORY_TABLE}
716
+ WHERE scopeId = $scopeId ${typeFilter}
717
+ AND archivedAt IS NONE AND (validUntil IS NONE OR validUntil > time::now())
718
+ ORDER BY createdAt DESC
719
+ `
720
+
721
+ const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql, { scopeId, memoryType }))
722
+
723
+ return results.map((row) => mapRowToMemoryRecord(row))
724
+ }
725
+
726
+ async findByHash(hash: string): Promise<MemoryRecord | null> {
727
+ const sql = `SELECT * FROM ${MEMORY_TABLE} WHERE hash = $hash`
728
+ const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql, { hash }))
729
+
730
+ const row = results.at(0)
731
+ if (!row) return null
732
+ return mapRowToMemoryRecord(row)
733
+ }
734
+
735
+ async addRelation(fromId: string, toId: string, relationType: RelationType, confidence: number = 1.0): Promise<void> {
736
+ confidence = Math.max(0, Math.min(1, confidence))
737
+ const fromRef = ensureRecordId(fromId, TABLES.MEMORY)
738
+ const toRef = ensureRecordId(toId, TABLES.MEMORY)
739
+ await databaseService.relate(fromRef, MEMORY_RELATION_TABLE, toRef, { relationType, confidence })
740
+
741
+ if (relationType === 'supersedes') {
742
+ await databaseService.query(
743
+ new BoundQuery(`UPDATE ${MEMORY_TABLE} SET validUntil = time::now() WHERE id = $toId AND validUntil IS NONE`, {
744
+ toId: toRef,
745
+ }),
746
+ )
747
+ await this.flagDependentsForReview(toRef)
748
+ }
749
+ }
750
+
751
+ private async flagDependentsForReview(supersededId: RecordIdRef): Promise<void> {
752
+ const dependents = await databaseService.query<{ id: RecordIdInput }>(
753
+ new BoundQuery(
754
+ `SELECT id FROM ${MEMORY_TABLE}
755
+ WHERE ->${MEMORY_RELATION_TABLE}[WHERE relationType = 'depends_on']->${MEMORY_TABLE} CONTAINS $supersededId
756
+ AND archivedAt IS NONE
757
+ AND needsReview = false`,
758
+ { supersededId },
759
+ ),
760
+ )
761
+
762
+ if (dependents.length === 0) return
763
+
764
+ const ids = dependents.map((d) => ensureRecordId(d.id, TABLES.MEMORY))
765
+ await databaseService.updateWhere(MEMORY_TABLE, inside('id', ids), { needsReview: true })
766
+ aiLogger.debug`Flagged ${dependents.length} dependent memories for review after supersede`
767
+ }
768
+
769
+ async getRelatedMemories(
770
+ memoryId: string,
771
+ relationType?: RelationType,
772
+ ): Promise<{ relatesTo: MemoryRecord[]; relatedBy: MemoryRecord[] }> {
773
+ const typeFilter = relationType ? `[WHERE relationType = $relationType]` : ''
774
+
775
+ const sql = `
776
+ SELECT
777
+ ->${MEMORY_RELATION_TABLE}${typeFilter}->${MEMORY_TABLE}.* AS relatesTo,
778
+ <-${MEMORY_RELATION_TABLE}${typeFilter}<-${MEMORY_TABLE}.* AS relatedBy
779
+ FROM ONLY $memoryId
780
+ `
781
+
782
+ const result = await databaseService.query<{ relatesTo: SurrealMemoryRow[]; relatedBy: SurrealMemoryRow[] }>(
783
+ new BoundQuery(sql, { memoryId: ensureRecordId(memoryId, TABLES.MEMORY), relationType }),
784
+ )
785
+
786
+ const data = result[0] ?? { relatesTo: [], relatedBy: [] }
787
+ return {
788
+ relatesTo: data.relatesTo.map((row) => mapRowToMemoryRecord(row)),
789
+ relatedBy: data.relatedBy.map((row) => mapRowToMemoryRecord(row)),
790
+ }
791
+ }
792
+
793
+ async findConflicts(scopeId: string): Promise<Array<{ memory: MemoryRecord; contradictedBy: MemoryRecord[] }>> {
794
+ const sql = `
795
+ SELECT
796
+ *,
797
+ <-${MEMORY_RELATION_TABLE}[WHERE relationType = 'contradicts']<-${MEMORY_TABLE}.* AS contradictedBy
798
+ FROM ${MEMORY_TABLE}
799
+ WHERE scopeId = $scopeId
800
+ AND count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'contradicts']) > 0
801
+ `
802
+
803
+ const results = await databaseService.query<SurrealMemoryRow & { contradictedBy: SurrealMemoryRow[] }>(
804
+ new BoundQuery(sql, { scopeId }),
805
+ )
806
+
807
+ return results.map((row) => ({
808
+ memory: mapRowToMemoryRecord(row),
809
+ contradictedBy: row.contradictedBy.map((r) => mapRowToMemoryRecord(r)),
810
+ }))
811
+ }
812
+
813
+ async graphWalk(
814
+ startId: string,
815
+ depth = 2,
816
+ ): Promise<{
817
+ memories: MemoryRecord[]
818
+ edges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }>
819
+ }> {
820
+ const maxDepth = Math.min(depth, 3)
821
+
822
+ const visited = new Set<string>([startId])
823
+ const allEdges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }> = []
824
+ const allMemories: MemoryRecord[] = []
825
+
826
+ let frontier = [startId]
827
+
828
+ for (let hop = 0; hop < maxDepth && frontier.length > 0; hop++) {
829
+ const nextFrontier: string[] = []
830
+
831
+ for (const nodeId of frontier) {
832
+ const sql = `
833
+ SELECT
834
+ ->${MEMORY_RELATION_TABLE}.{in AS from, out AS to, relationType, confidence} AS outEdges,
835
+ ->${MEMORY_RELATION_TABLE}->${MEMORY_TABLE}[WHERE archivedAt IS NONE].* AS outMemories,
836
+ <-${MEMORY_RELATION_TABLE}.{in AS from, out AS to, relationType, confidence} AS inEdges,
837
+ <-${MEMORY_RELATION_TABLE}<-${MEMORY_TABLE}[WHERE archivedAt IS NONE].* AS inMemories
838
+ FROM ONLY $nodeId
839
+ `
840
+ const results = await databaseService.query<{
841
+ outEdges: Array<{ from: RecordIdInput; to: RecordIdInput; relationType: RelationType; confidence: number }>
842
+ outMemories: SurrealMemoryRow[]
843
+ inEdges: Array<{ from: RecordIdInput; to: RecordIdInput; relationType: RelationType; confidence: number }>
844
+ inMemories: SurrealMemoryRow[]
845
+ }>(new BoundQuery(sql, { nodeId: ensureRecordId(nodeId, TABLES.MEMORY) }))
846
+
847
+ const row = results.at(0)
848
+ if (row) {
849
+ for (const edge of row.outEdges) {
850
+ allEdges.push({
851
+ from: recordIdToString(edge.from, TABLES.MEMORY),
852
+ to: recordIdToString(edge.to, TABLES.MEMORY),
853
+ relationType: edge.relationType,
854
+ confidence: edge.confidence,
855
+ })
856
+ }
857
+ for (const edge of row.inEdges) {
858
+ allEdges.push({
859
+ from: recordIdToString(edge.from, TABLES.MEMORY),
860
+ to: recordIdToString(edge.to, TABLES.MEMORY),
861
+ relationType: edge.relationType,
862
+ confidence: edge.confidence,
863
+ })
864
+ }
865
+ for (const mem of [...row.outMemories, ...row.inMemories]) {
866
+ const memoryId = recordIdToString(mem.id, TABLES.MEMORY)
867
+ if (!visited.has(memoryId)) {
868
+ visited.add(memoryId)
869
+ allMemories.push(mapRowToMemoryRecord(mem))
870
+ nextFrontier.push(memoryId)
871
+ }
872
+ }
873
+ }
874
+ }
875
+
876
+ frontier = nextFrontier
877
+ }
878
+
879
+ return { memories: allMemories, edges: allEdges }
880
+ }
881
+
882
+ private async recordHistory(
883
+ memoryId: string,
884
+ prevValue: string | null,
885
+ newValue: string | null,
886
+ event: MemoryEvent,
887
+ ): Promise<void> {
888
+ const memoryRef = ensureRecordId(memoryId, TABLES.MEMORY)
889
+ const historyRow: Record<string, unknown> = {
890
+ memoryId: memoryRef,
891
+ event,
892
+ ...(prevValue === null ? {} : { prevValue }),
893
+ ...(newValue === null ? {} : { newValue }),
894
+ }
895
+ await databaseService.insert<Record<string, unknown>>(MEMORY_HISTORY_TABLE, historyRow)
896
+ }
897
+
898
+ async enrichWithNeighbors(results: MemorySearchResult[], topN: number = 5): Promise<MemorySearchResult[]> {
899
+ const topIds = results.slice(0, topN).map((r) => r.id)
900
+ if (topIds.length === 0) return results
901
+
902
+ const topRefs = topIds.map((id) => ensureRecordId(id, TABLES.MEMORY))
903
+
904
+ const sql = `
905
+ SELECT
906
+ id,
907
+ ->${MEMORY_RELATION_TABLE}->(${MEMORY_TABLE} WHERE archivedAt IS NONE).{id, content} AS outgoing,
908
+ <-${MEMORY_RELATION_TABLE}<-(${MEMORY_TABLE} WHERE archivedAt IS NONE).{id, content} AS incoming
909
+ FROM ${MEMORY_TABLE}
910
+ WHERE id IN $ids
911
+ `
912
+
913
+ const rows = await databaseService.query<{
914
+ id: RecordIdInput
915
+ outgoing: Array<{ id: RecordIdInput; content: string; relationType?: string }> | null
916
+ incoming: Array<{ id: RecordIdInput; content: string; relationType?: string }> | null
917
+ }>(new BoundQuery(sql, { ids: topRefs }))
918
+
919
+ const neighborMap = new Map<string, string[]>()
920
+ for (const row of rows) {
921
+ const rowId = recordIdToString(row.id, TABLES.MEMORY)
922
+ const contexts: string[] = []
923
+ const seen = new Set<string>()
924
+ for (const neighbor of [...(row.outgoing ?? []), ...(row.incoming ?? [])]) {
925
+ const neighborId = recordIdToString(neighbor.id, TABLES.MEMORY)
926
+ if (!neighbor.content || seen.has(neighborId)) continue
927
+ seen.add(neighborId)
928
+ const label = neighbor.relationType ? `[${neighbor.relationType}]` : ''
929
+ const truncated = neighbor.content.length > 200 ? `${neighbor.content.slice(0, 197)}...` : neighbor.content
930
+ contexts.push(`${label} ${truncated}`.trim())
931
+ }
932
+ if (contexts.length > 0) {
933
+ neighborMap.set(rowId, contexts)
934
+ }
935
+ }
936
+
937
+ return results.map((result) => {
938
+ const neighbors = neighborMap.get(result.id)
939
+ if (!neighbors) return result
940
+ return { ...result, metadata: { ...result.metadata, relatedContext: neighbors } }
941
+ })
942
+ }
943
+
944
+ async getStaleMemories(scopeId: string, limit: number = 5): Promise<MemorySearchResult[]> {
945
+ const results = await databaseService.query<BasicSearchRow>(
946
+ new BoundQuery(
947
+ `SELECT id, content, metadata
948
+ FROM ${MEMORY_TABLE}
949
+ WHERE scopeId = $scopeId
950
+ AND needsReview = true
951
+ AND archivedAt IS NONE
952
+ ORDER BY updatedAt DESC
953
+ LIMIT $limit`,
954
+ { scopeId, limit },
955
+ ),
956
+ )
957
+
958
+ return results.map((row) => ({
959
+ id: recordIdToString(row.id, TABLES.MEMORY),
960
+ content: row.content,
961
+ score: 0,
962
+ metadata: { ...row.metadata, needsReview: true },
963
+ }))
964
+ }
965
+ }
966
+
967
+ let defaultMemoryStore: SurrealMemoryStore | null = null
968
+
969
+ export function getDefaultMemoryStore(): SurrealMemoryStore {
970
+ if (!defaultMemoryStore) {
971
+ defaultMemoryStore = new SurrealMemoryStore(createDefaultEmbeddings())
972
+ }
973
+ return defaultMemoryStore
974
+ }