@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.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- 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
|
+
}
|