@lota-sdk/core 0.4.7 → 0.4.9
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/package.json +11 -12
- package/src/ai/embedding-cache.ts +94 -22
- package/src/ai-gateway/ai-gateway.ts +738 -223
- package/src/config/agent-defaults.ts +176 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/constants.ts +8 -2
- package/src/config/logger.ts +286 -19
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -383
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +856 -598
- package/src/db/memory.ts +398 -275
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +255 -0
- package/src/db/service.ts +726 -761
- package/src/db/startup.ts +140 -66
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +87 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +98 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/layers.ts +228 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +31 -0
- package/src/effect/services.ts +57 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +122 -71
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +29 -0
- package/src/queues/autonomous-job.queue.ts +130 -74
- package/src/queues/context-compaction.queue.ts +60 -15
- package/src/queues/delayed-node-promotion.queue.ts +52 -15
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +13 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
- package/src/queues/plan-scheduler.queue.ts +107 -31
- package/src/queues/post-chat-memory.queue.ts +66 -24
- package/src/queues/queue-factory.ts +142 -52
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +54 -9
- package/src/redis/connection.ts +84 -32
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +10 -0
- package/src/redis/stream-context.ts +84 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +4 -1
- package/src/runtime/agent-stream-helpers.ts +20 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +11 -7
- package/src/runtime/helper-model.ts +135 -48
- package/src/runtime/index.ts +7 -7
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
- package/src/runtime/plugin-resolution.ts +144 -24
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +197 -130
- package/src/runtime/retrieval-adapters.ts +38 -4
- package/src/runtime/runtime-config.ts +165 -61
- package/src/runtime/runtime-extensions.ts +21 -34
- package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
- package/src/runtime/social-chat/social-chat.ts +594 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +172 -94
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +329 -217
- package/src/services/artifact.service.ts +225 -148
- package/src/services/attachment.service.ts +137 -115
- package/src/services/autonomous-job.service.ts +888 -491
- package/src/services/chat-run-registry.service.ts +11 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +162 -90
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +256 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +80 -170
- package/src/services/graph-full-routing.ts +182 -0
- package/src/services/index.ts +18 -20
- package/src/services/institutional-memory.service.ts +220 -123
- package/src/services/learned-skill.service.ts +364 -259
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-org-memory.ts +39 -0
- package/src/services/memory/memory-preseeded.ts +80 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
- package/src/services/memory/memory.service.ts +692 -0
- package/src/services/memory/rerank.service.ts +209 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +17 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +144 -51
- package/src/services/ownership-dispatcher.service.ts +415 -264
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/plan/plan-approval.service.ts +102 -0
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +175 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +398 -0
- package/src/services/plan/plan-deadline.service.ts +547 -0
- package/src/services/plan/plan-event-delivery.service.ts +261 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +475 -0
- package/src/services/plan/plan-executor-helpers.ts +322 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1654 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +644 -0
- package/src/services/plan/plan-scheduler.service.ts +385 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +33 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +125 -0
- package/src/services/plugin-executor.service.ts +97 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +296 -230
- package/src/services/recent-activity-title.service.ts +65 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +176 -125
- package/src/services/system-executor.service.ts +91 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +369 -0
- package/src/services/thread/thread-listing.ts +198 -0
- package/src/services/thread/thread-memory-block.ts +117 -0
- package/src/services/thread/thread-message.service.ts +363 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
- package/src/services/thread/thread-turn-streaming.ts +402 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +343 -0
- package/src/services/thread/thread.service.ts +335 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +331 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +2 -2
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/memory-reranker.agent.ts +2 -2
- package/src/system-agents/memory.agent.ts +2 -2
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
- package/src/system-agents/skill-extractor.agent.ts +2 -2
- package/src/system-agents/skill-manager.agent.ts +2 -2
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +220 -161
- package/src/tools/fetch-webpage.tool.ts +21 -17
- package/src/tools/firecrawl-client.ts +16 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +49 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +26 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +124 -83
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +17 -23
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +95 -16
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +186 -51
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +56 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -844
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-approval.service.ts +0 -83
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
package/src/db/memory-store.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { Schema, Duration, Effect, Metric, Schedule } from 'effect'
|
|
1
2
|
import { BoundQuery, eq, inside } from 'surrealdb'
|
|
2
3
|
|
|
3
4
|
import { aiLogger } from '../config/logger'
|
|
4
5
|
import { DEFAULT_MEMORY_SEARCH_LIMIT } from '../config/search'
|
|
5
|
-
import {
|
|
6
|
-
import { withTimeout } from '../utils/async'
|
|
6
|
+
import { ProviderEmbeddings } from '../embeddings/provider'
|
|
7
7
|
import { clampImportance, truncateText } from '../utils/string'
|
|
8
8
|
import { memoryQueryBuilder } from './memory-query-builder'
|
|
9
9
|
import type { RelationCounts } from './memory-store.helpers'
|
|
@@ -19,8 +19,10 @@ import type {
|
|
|
19
19
|
} from './memory-types'
|
|
20
20
|
import { ensureRecordId, recordIdToString } from './record-id'
|
|
21
21
|
import type { RecordIdInput, RecordIdRef } from './record-id'
|
|
22
|
-
import {
|
|
22
|
+
import type { SurrealDBService } from './service'
|
|
23
|
+
import type { SurrealDBError } from './service-normalization'
|
|
23
24
|
import { TABLES } from './tables'
|
|
25
|
+
import { isRetriableTransactionConflict } from './transaction-conflict'
|
|
24
26
|
|
|
25
27
|
const MEMORY_TABLE = TABLES.MEMORY
|
|
26
28
|
const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
|
|
@@ -30,27 +32,66 @@ const STRONG_GRAPH_BOOSTS = { support: 0.1, contradict: 0.2 } as const
|
|
|
30
32
|
const WEAK_GRAPH_BOOSTS = { support: 0.05, contradict: 0.1 } as const
|
|
31
33
|
const CANDIDATE_FANOUT_MULTIPLIER = 4
|
|
32
34
|
const CANDIDATE_SLICE_FLOOR = 50
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const memorySearchDuration = Metric.histogram('memory_search_duration_ms', {
|
|
36
|
+
boundaries: Metric.boundariesFromIterable([10, 50, 100, 250, 500, 1000, 2000]),
|
|
37
|
+
})
|
|
38
|
+
const TOUCH_MEMORIES_RETRY_OPTIONS = {
|
|
39
|
+
times: 3,
|
|
40
|
+
schedule: Schedule.jittered(Schedule.exponential(Duration.millis(25), 2)),
|
|
41
|
+
while: (error: unknown) => isRetriableTransactionConflict(error),
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
class MemoryStoreError extends Schema.TaggedErrorClass<MemoryStoreError>()('MemoryStoreError', {
|
|
45
|
+
message: Schema.String,
|
|
46
|
+
cause: Schema.Defect,
|
|
47
|
+
}) {}
|
|
48
|
+
|
|
49
|
+
function recordMemorySearchDuration(elapsed: number): void {
|
|
50
|
+
void Effect.runFork(Metric.update(memorySearchDuration, elapsed))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tryMemoryStorePromise<A, R = never>(
|
|
54
|
+
message: string,
|
|
55
|
+
thunk: () => PromiseLike<A> | Effect.Effect<A, unknown, R>,
|
|
56
|
+
): Effect.Effect<A, MemoryStoreError, R> {
|
|
57
|
+
return Effect.suspend(() => {
|
|
58
|
+
try {
|
|
59
|
+
const value = thunk()
|
|
60
|
+
if (Effect.isEffect(value)) {
|
|
61
|
+
return value.pipe(Effect.mapError((cause) => new MemoryStoreError({ message, cause })))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Effect.tryPromise({ try: () => value, catch: (cause) => new MemoryStoreError({ message, cause }) })
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
return Effect.fail(new MemoryStoreError({ message, cause }))
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
36
70
|
|
|
37
71
|
interface EmbeddingClient {
|
|
38
72
|
embedQuery(text: string): Promise<number[]>
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
export class SurrealMemoryStore {
|
|
42
|
-
|
|
76
|
+
private db: SurrealDBService
|
|
77
|
+
private embeddings: EmbeddingClient
|
|
78
|
+
constructor(db: SurrealDBService, embeddings: EmbeddingClient) {
|
|
79
|
+
this.db = db
|
|
80
|
+
this.embeddings = embeddings
|
|
81
|
+
}
|
|
43
82
|
|
|
44
|
-
private
|
|
83
|
+
private toMetadataFieldPathEffect(key: string): Effect.Effect<string, MemoryStoreError> {
|
|
45
84
|
const segments = key.split('.').map((segment) => segment.trim())
|
|
46
85
|
if (
|
|
47
86
|
segments.length === 0 ||
|
|
48
87
|
segments.some((segment) => segment.length === 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment))
|
|
49
88
|
) {
|
|
50
|
-
|
|
89
|
+
return Effect.fail(
|
|
90
|
+
new MemoryStoreError({ message: `Invalid memory metadata filter key: ${key}`, cause: undefined }),
|
|
91
|
+
)
|
|
51
92
|
}
|
|
52
93
|
|
|
53
|
-
return `metadata.${segments.join('.')}`
|
|
94
|
+
return Effect.succeed(`metadata.${segments.join('.')}`)
|
|
54
95
|
}
|
|
55
96
|
|
|
56
97
|
private tokenizeQuery(query: string): string[] {
|
|
@@ -79,11 +120,11 @@ export class SurrealMemoryStore {
|
|
|
79
120
|
return score
|
|
80
121
|
}
|
|
81
122
|
|
|
82
|
-
private
|
|
123
|
+
private listRecentBasic(options: {
|
|
83
124
|
scopeId: string
|
|
84
125
|
limit: number
|
|
85
126
|
memoryType?: MemoryRecord['memoryType']
|
|
86
|
-
}):
|
|
127
|
+
}): Effect.Effect<BasicSearchRow[], SurrealDBError, never> {
|
|
87
128
|
const typeFilter = options.memoryType ? 'AND memoryType = $memoryType' : ''
|
|
88
129
|
const sql = `
|
|
89
130
|
SELECT id, content, metadata, createdAt
|
|
@@ -94,18 +135,18 @@ export class SurrealMemoryStore {
|
|
|
94
135
|
LIMIT $limit
|
|
95
136
|
`
|
|
96
137
|
|
|
97
|
-
return
|
|
138
|
+
return this.db.query<BasicSearchRow>(
|
|
98
139
|
new BoundQuery(sql, { scopeId: options.scopeId, memoryType: options.memoryType, limit: options.limit }),
|
|
99
140
|
)
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
|
|
143
|
+
private listTopMemoriesEffect(options: {
|
|
103
144
|
scopeId: string
|
|
104
145
|
limit: number
|
|
105
146
|
memoryType?: MemoryRecord['memoryType']
|
|
106
147
|
durability?: MemoryRecord['durability']
|
|
107
148
|
minImportance?: number
|
|
108
|
-
}):
|
|
149
|
+
}): Effect.Effect<MemoryRecord[], MemoryStoreError, never> {
|
|
109
150
|
const typeFilter = options.memoryType ? 'AND memoryType = $memoryType' : ''
|
|
110
151
|
const durabilityFilter = options.durability ? 'AND durability = $durability' : ''
|
|
111
152
|
const importanceFilter = typeof options.minImportance === 'number' ? 'AND importance >= $minImportance' : ''
|
|
@@ -118,26 +159,36 @@ export class SurrealMemoryStore {
|
|
|
118
159
|
LIMIT $limit
|
|
119
160
|
`
|
|
120
161
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
return tryMemoryStorePromise('Failed to list top memories.', () =>
|
|
163
|
+
this.db.query<SurrealMemoryRow>(
|
|
164
|
+
new BoundQuery(sql, {
|
|
165
|
+
scopeId: options.scopeId,
|
|
166
|
+
memoryType: options.memoryType,
|
|
167
|
+
durability: options.durability,
|
|
168
|
+
minImportance: options.minImportance,
|
|
169
|
+
limit: options.limit,
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
).pipe(Effect.map((rows) => rows.map((row: SurrealMemoryRow) => mapRowToMemoryRecord(row))))
|
|
173
|
+
}
|
|
130
174
|
|
|
131
|
-
|
|
175
|
+
listTopMemories(options: {
|
|
176
|
+
scopeId: string
|
|
177
|
+
limit: number
|
|
178
|
+
memoryType?: MemoryRecord['memoryType']
|
|
179
|
+
durability?: MemoryRecord['durability']
|
|
180
|
+
minImportance?: number
|
|
181
|
+
}): Effect.Effect<MemoryRecord[], MemoryStoreError, never> {
|
|
182
|
+
return this.listTopMemoriesEffect(options)
|
|
132
183
|
}
|
|
133
184
|
|
|
134
|
-
private
|
|
185
|
+
private vectorSearchWithEmbeddingEffect(options: {
|
|
135
186
|
embedding: number[]
|
|
136
187
|
scopeId: string
|
|
137
188
|
limit: number
|
|
138
189
|
memoryType?: MemoryRecord['memoryType']
|
|
139
190
|
fastMode?: boolean
|
|
140
|
-
}):
|
|
191
|
+
}): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
141
192
|
const { sql, bindVars } = memoryQueryBuilder.buildVectorSearch({
|
|
142
193
|
embedding: options.embedding,
|
|
143
194
|
scopeId: options.scopeId,
|
|
@@ -145,26 +196,28 @@ export class SurrealMemoryStore {
|
|
|
145
196
|
memoryType: options.memoryType,
|
|
146
197
|
})
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
|
|
199
|
+
return Effect.gen(
|
|
200
|
+
function* (this: SurrealMemoryStore) {
|
|
201
|
+
const results = yield* this.queryFinalStatementEffect<BasicSearchRow & { distance: number }>(sql, bindVars)
|
|
202
|
+
if (results.length === 0) return []
|
|
203
|
+
if (options.fastMode) {
|
|
204
|
+
return this.mapFastRows(results, options.limit, (row) => 1 / (1 + row.distance))
|
|
205
|
+
}
|
|
156
206
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
const relationCounts = yield* this.fetchRelationCountsBatchEffect(results.map((row) => row.id))
|
|
208
|
+
const processed = processGraphAwareRows(
|
|
209
|
+
results,
|
|
210
|
+
relationCounts,
|
|
211
|
+
options.limit,
|
|
212
|
+
(row) => 1 / (1 + row.distance),
|
|
213
|
+
STRONG_GRAPH_BOOSTS,
|
|
214
|
+
MIN_RELEVANCE_SCORE,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
this.touchMemories(processed.map((row) => row.id))
|
|
218
|
+
return processed
|
|
219
|
+
}.bind(this),
|
|
164
220
|
)
|
|
165
|
-
|
|
166
|
-
this.touchMemories(processed.map((row) => row.id))
|
|
167
|
-
return processed
|
|
168
221
|
}
|
|
169
222
|
|
|
170
223
|
private mapFastRows<T extends BasicSearchRow>(
|
|
@@ -182,20 +235,22 @@ export class SurrealMemoryStore {
|
|
|
182
235
|
}))
|
|
183
236
|
}
|
|
184
237
|
|
|
185
|
-
private
|
|
238
|
+
private generateEmbeddingEffect(content: string): Effect.Effect<number[], MemoryStoreError, never> {
|
|
186
239
|
const normalized = content.trim()
|
|
187
|
-
if (!normalized) return []
|
|
240
|
+
if (!normalized) return Effect.succeed([])
|
|
188
241
|
|
|
189
|
-
return this.embeddings.embedQuery(normalized)
|
|
242
|
+
return tryMemoryStorePromise('Failed to generate memory embedding.', () => this.embeddings.embedQuery(normalized))
|
|
190
243
|
}
|
|
191
244
|
|
|
192
|
-
|
|
193
|
-
|
|
245
|
+
warmEmbedding(content: string): Effect.Effect<void, MemoryStoreError, never> {
|
|
246
|
+
return Effect.asVoid(this.generateEmbeddingEffect(content))
|
|
194
247
|
}
|
|
195
248
|
|
|
196
|
-
private
|
|
249
|
+
private fetchRelationCountsBatchEffect(
|
|
250
|
+
memoryIds: RecordIdInput[],
|
|
251
|
+
): Effect.Effect<Map<string, RelationCounts>, MemoryStoreError, never> {
|
|
197
252
|
if (memoryIds.length === 0) {
|
|
198
|
-
return new Map()
|
|
253
|
+
return Effect.succeed(new Map<string, RelationCounts>())
|
|
199
254
|
}
|
|
200
255
|
|
|
201
256
|
const memoryRefs = memoryIds.map((id) => ensureRecordId(id, TABLES.MEMORY))
|
|
@@ -211,27 +266,33 @@ export class SurrealMemoryStore {
|
|
|
211
266
|
WHERE id IN $memoryIds
|
|
212
267
|
`
|
|
213
268
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
269
|
+
return tryMemoryStorePromise('Failed to fetch relation counts.', () =>
|
|
270
|
+
this.db.query<{
|
|
271
|
+
id: RecordIdInput
|
|
272
|
+
supersedeCount: number
|
|
273
|
+
contradictCount: number
|
|
274
|
+
supportCount: number
|
|
275
|
+
contradictionTexts: string[] | null
|
|
276
|
+
}>(new BoundQuery(sql, { memoryIds: memoryRefs })),
|
|
277
|
+
).pipe(
|
|
278
|
+
Effect.map((results) => {
|
|
279
|
+
const countsMap = new Map<string, RelationCounts>()
|
|
280
|
+
for (const row of results) {
|
|
281
|
+
const rawTexts = row.contradictionTexts ?? []
|
|
282
|
+
const contradictions = rawTexts.filter(
|
|
283
|
+
(text: string | null): text is string => typeof text === 'string' && text.length > 0,
|
|
284
|
+
)
|
|
285
|
+
countsMap.set(recordIdToString(row.id, TABLES.MEMORY), {
|
|
286
|
+
supersedeCount: row.supersedeCount,
|
|
287
|
+
contradictCount: row.contradictCount,
|
|
288
|
+
supportCount: row.supportCount,
|
|
289
|
+
contradictions,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
233
292
|
|
|
234
|
-
|
|
293
|
+
return countsMap
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
235
296
|
}
|
|
236
297
|
|
|
237
298
|
private touchMemories(memoryIds: string[]): void {
|
|
@@ -245,43 +306,36 @@ export class SurrealMemoryStore {
|
|
|
245
306
|
`
|
|
246
307
|
const query = new BoundQuery(sql, { memoryIds: memoryRefs })
|
|
247
308
|
|
|
248
|
-
void this.runTouchMemoriesWithRetry(query)
|
|
309
|
+
void Effect.runFork(this.runTouchMemoriesWithRetry(query))
|
|
249
310
|
}
|
|
250
311
|
|
|
251
|
-
private
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const backoffMs =
|
|
265
|
-
TOUCH_MEMORIES_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) +
|
|
266
|
-
Math.floor(Math.random() * TOUCH_MEMORIES_RETRY_JITTER_MS)
|
|
267
|
-
await Bun.sleep(backoffMs)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private isRetriableTransactionConflict(error: unknown): boolean {
|
|
273
|
-
if (!(error instanceof Error)) return false
|
|
274
|
-
const message = error.message.toLowerCase()
|
|
275
|
-
return message.includes('transaction conflict') || message.includes('this transaction can be retried')
|
|
312
|
+
private runTouchMemoriesWithRetry(query: BoundQuery): Effect.Effect<void, never, never> {
|
|
313
|
+
return this.db.query(query).pipe(
|
|
314
|
+
Effect.retry(TOUCH_MEMORIES_RETRY_OPTIONS),
|
|
315
|
+
Effect.catch((error) =>
|
|
316
|
+
Effect.sync(() => {
|
|
317
|
+
aiLogger.warn`Failed to update memory access counters: ${error}`
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
320
|
+
Effect.asVoid,
|
|
321
|
+
)
|
|
276
322
|
}
|
|
277
323
|
|
|
278
|
-
private
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
324
|
+
private queryFinalStatementEffect<T>(
|
|
325
|
+
sql: string,
|
|
326
|
+
bindVars: Record<string, unknown>,
|
|
327
|
+
): Effect.Effect<T[], MemoryStoreError, never> {
|
|
328
|
+
return tryMemoryStorePromise('Failed to query memory statements.', () =>
|
|
329
|
+
this.db.queryAll<unknown>(new BoundQuery(sql, bindVars)),
|
|
330
|
+
).pipe(
|
|
331
|
+
Effect.map((statements) => {
|
|
332
|
+
const finalStatement = statements.at(-1)
|
|
333
|
+
return Array.isArray(finalStatement) ? (finalStatement as T[]) : []
|
|
334
|
+
}),
|
|
335
|
+
)
|
|
282
336
|
}
|
|
283
337
|
|
|
284
|
-
private
|
|
338
|
+
private fallbackWeightedSearchEffect(
|
|
285
339
|
query: string,
|
|
286
340
|
tokens: string[],
|
|
287
341
|
options: {
|
|
@@ -292,123 +346,143 @@ export class SurrealMemoryStore {
|
|
|
292
346
|
reason: string
|
|
293
347
|
fastMode?: boolean
|
|
294
348
|
},
|
|
295
|
-
):
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
.slice(0, Math.max(options.limit * CANDIDATE_FANOUT_MULTIPLIER, CANDIDATE_SLICE_FLOOR))
|
|
327
|
-
|
|
328
|
-
if (options.fastMode) {
|
|
329
|
-
return this.mapFastRows(scoredRows, options.limit, (row) => row.textScore)
|
|
330
|
-
}
|
|
349
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
350
|
+
return Effect.gen(
|
|
351
|
+
function* (this: SurrealMemoryStore) {
|
|
352
|
+
aiLogger.debug`Weighted hybrid search fallback to vector/recent (scopeId: ${options.scopeId}, reason: ${options.reason})`
|
|
353
|
+
const vectorResults = yield* this.vectorSearchWithEmbeddingEffect({
|
|
354
|
+
embedding: options.embedding,
|
|
355
|
+
scopeId: options.scopeId,
|
|
356
|
+
limit: options.limit,
|
|
357
|
+
memoryType: options.memoryType,
|
|
358
|
+
fastMode: options.fastMode,
|
|
359
|
+
})
|
|
360
|
+
if (vectorResults.length > 0) return vectorResults
|
|
361
|
+
|
|
362
|
+
const recentLimit = Math.max(50, options.limit * 10)
|
|
363
|
+
const recent = yield* tryMemoryStorePromise('Failed to list recent memories.', () =>
|
|
364
|
+
this.listRecentBasic({ scopeId: options.scopeId, limit: recentLimit, memoryType: options.memoryType }),
|
|
365
|
+
)
|
|
366
|
+
if (recent.length === 0) return []
|
|
367
|
+
|
|
368
|
+
const scoredRows = recent
|
|
369
|
+
.map((row, index) => ({ ...row, index, textScore: this.scoreTextMatch(row.content, tokens, query) }))
|
|
370
|
+
.sort((a, b) => {
|
|
371
|
+
if (b.textScore !== a.textScore) return b.textScore - a.textScore
|
|
372
|
+
return a.index - b.index
|
|
373
|
+
})
|
|
374
|
+
.slice(0, Math.max(options.limit * CANDIDATE_FANOUT_MULTIPLIER, CANDIDATE_SLICE_FLOOR))
|
|
375
|
+
|
|
376
|
+
if (options.fastMode) {
|
|
377
|
+
return this.mapFastRows(scoredRows, options.limit, (row) => row.textScore)
|
|
378
|
+
}
|
|
331
379
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
const recentRelationCounts = yield* this.fetchRelationCountsBatchEffect(scoredRows.map((row) => row.id))
|
|
381
|
+
const processed = processGraphAwareRows(
|
|
382
|
+
scoredRows,
|
|
383
|
+
recentRelationCounts,
|
|
384
|
+
options.limit,
|
|
385
|
+
(row) => row.textScore,
|
|
386
|
+
WEAK_GRAPH_BOOSTS,
|
|
387
|
+
MIN_RELEVANCE_SCORE,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
this.touchMemories(processed.map((row) => row.id))
|
|
391
|
+
return processed
|
|
392
|
+
}.bind(this),
|
|
341
393
|
)
|
|
342
|
-
|
|
343
|
-
this.touchMemories(processed.map((row) => row.id))
|
|
344
|
-
return processed
|
|
345
394
|
}
|
|
346
395
|
|
|
347
396
|
private static WRITE_DEDUP_THRESHOLD = 0.9
|
|
348
397
|
|
|
349
|
-
|
|
398
|
+
private insertEffect(
|
|
350
399
|
content: string,
|
|
351
400
|
scopeId: string,
|
|
352
401
|
memoryType: MemoryRecord['memoryType'],
|
|
353
402
|
metadata: Record<string, unknown> = {},
|
|
354
403
|
importance: number = 1,
|
|
355
404
|
durability: MemoryRecord['durability'] = 'standard',
|
|
356
|
-
):
|
|
405
|
+
): Effect.Effect<string, MemoryStoreError, never> {
|
|
357
406
|
const hash = hashContent(content, scopeId, memoryType)
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const result = await databaseService.insert<{ id: RecordIdInput }>(MEMORY_TABLE, {
|
|
373
|
-
content,
|
|
374
|
-
embedding,
|
|
375
|
-
hash,
|
|
376
|
-
scopeId,
|
|
377
|
-
memoryType,
|
|
378
|
-
metadata,
|
|
379
|
-
importance,
|
|
380
|
-
durability,
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
const id = result[0]?.id ? recordIdToString(result[0].id, TABLES.MEMORY) : ''
|
|
407
|
+
const normalizedImportance = clampImportance(importance)
|
|
408
|
+
|
|
409
|
+
return Effect.gen(
|
|
410
|
+
function* (this: SurrealMemoryStore) {
|
|
411
|
+
const embedding = yield* this.generateEmbeddingEffect(content)
|
|
412
|
+
const nearDup = yield* this.findNearDuplicateEffect(embedding, scopeId, content)
|
|
413
|
+
if (nearDup) {
|
|
414
|
+
const mergedImportance = clampImportance(Math.max(nearDup.importance, normalizedImportance))
|
|
415
|
+
const keepNew = content.length >= nearDup.content.length
|
|
416
|
+
const winnerContent = keepNew ? content : nearDup.content
|
|
417
|
+
yield* this.updateEffect(nearDup.id, winnerContent, { importance: mergedImportance })
|
|
418
|
+
aiLogger.debug`Write-time dedup: merged into existing memory ${nearDup.id} (similarity: ${nearDup.similarity.toFixed(3)})`
|
|
419
|
+
return nearDup.id
|
|
420
|
+
}
|
|
384
421
|
|
|
385
|
-
|
|
422
|
+
const result = yield* tryMemoryStorePromise('Failed to insert memory.', () =>
|
|
423
|
+
this.db.insert<{ id: RecordIdInput }>(MEMORY_TABLE, {
|
|
424
|
+
content,
|
|
425
|
+
embedding,
|
|
426
|
+
hash,
|
|
427
|
+
scopeId,
|
|
428
|
+
memoryType,
|
|
429
|
+
metadata,
|
|
430
|
+
importance: normalizedImportance,
|
|
431
|
+
durability,
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
const id = result[0]?.id ? recordIdToString(result[0].id, TABLES.MEMORY) : ''
|
|
435
|
+
yield* this.recordHistoryEffect(id, null, content, 'ADD')
|
|
436
|
+
return id
|
|
437
|
+
}.bind(this),
|
|
438
|
+
)
|
|
439
|
+
}
|
|
386
440
|
|
|
387
|
-
|
|
441
|
+
insert(
|
|
442
|
+
content: string,
|
|
443
|
+
scopeId: string,
|
|
444
|
+
memoryType: MemoryRecord['memoryType'],
|
|
445
|
+
metadata: Record<string, unknown> = {},
|
|
446
|
+
importance: number = 1,
|
|
447
|
+
durability: MemoryRecord['durability'] = 'standard',
|
|
448
|
+
): Effect.Effect<string, MemoryStoreError, never> {
|
|
449
|
+
return this.insertEffect(content, scopeId, memoryType, metadata, importance, durability)
|
|
388
450
|
}
|
|
389
451
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
452
|
+
private getByHashEffect(hash: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
|
|
453
|
+
return tryMemoryStorePromise('Failed to query memory by hash.', () =>
|
|
454
|
+
this.db.query<SurrealMemoryRow>(
|
|
455
|
+
new BoundQuery(
|
|
456
|
+
`
|
|
457
|
+
SELECT *
|
|
458
|
+
FROM ${MEMORY_TABLE}
|
|
459
|
+
WHERE hash = $hash
|
|
460
|
+
LIMIT 1
|
|
461
|
+
`,
|
|
462
|
+
{ hash },
|
|
463
|
+
),
|
|
400
464
|
),
|
|
465
|
+
).pipe(
|
|
466
|
+
Effect.map((rows) => {
|
|
467
|
+
const row = rows.at(0)
|
|
468
|
+
return row ? mapRowToMemoryRecord(row) : null
|
|
469
|
+
}),
|
|
401
470
|
)
|
|
471
|
+
}
|
|
402
472
|
|
|
403
|
-
|
|
404
|
-
return
|
|
473
|
+
getByHash(hash: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
|
|
474
|
+
return this.getByHashEffect(hash)
|
|
405
475
|
}
|
|
406
476
|
|
|
407
|
-
private
|
|
477
|
+
private findNearDuplicateEffect(
|
|
408
478
|
embedding: number[],
|
|
409
479
|
scopeId: string,
|
|
410
480
|
content: string,
|
|
411
|
-
):
|
|
481
|
+
): Effect.Effect<
|
|
482
|
+
{ id: string; content: string; importance: number; similarity: number } | null,
|
|
483
|
+
MemoryStoreError,
|
|
484
|
+
never
|
|
485
|
+
> {
|
|
412
486
|
const candidateLimit = 12
|
|
413
487
|
const sql = `
|
|
414
488
|
LET $candidateRows = (
|
|
@@ -434,109 +508,130 @@ export class SurrealMemoryStore {
|
|
|
434
508
|
LIMIT ${candidateLimit}
|
|
435
509
|
`
|
|
436
510
|
|
|
437
|
-
|
|
511
|
+
return this.queryFinalStatementEffect<{
|
|
438
512
|
id: RecordIdInput
|
|
439
513
|
content: string
|
|
440
514
|
importance: number
|
|
441
515
|
similarity: number
|
|
442
|
-
}>(sql, { scopeId, embedding })
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
516
|
+
}>(sql, { scopeId, embedding }).pipe(
|
|
517
|
+
Effect.map((neighborRows) => {
|
|
518
|
+
const neighbors = neighborRows.map((row) => ({ ...row, id: recordIdToString(row.id, TABLES.MEMORY) }))
|
|
519
|
+
|
|
520
|
+
for (const neighbor of neighbors) {
|
|
521
|
+
if (neighbor.similarity < SurrealMemoryStore.WRITE_DEDUP_THRESHOLD) break
|
|
522
|
+
const [shorter, longer] =
|
|
523
|
+
content.length <= neighbor.content.length ? [content, neighbor.content] : [neighbor.content, content]
|
|
524
|
+
const a = shorter.toLowerCase().trim()
|
|
525
|
+
const b = longer.toLowerCase().trim()
|
|
526
|
+
if (b.includes(a)) return neighbor
|
|
527
|
+
const aWords = new Set(a.split(/\s+/))
|
|
528
|
+
const bWords = new Set(b.split(/\s+/))
|
|
529
|
+
let overlap = 0
|
|
530
|
+
for (const word of aWords) {
|
|
531
|
+
if (bWords.has(word)) overlap++
|
|
532
|
+
}
|
|
533
|
+
if (aWords.size > 0 && overlap / aWords.size >= 0.8) return neighbor
|
|
534
|
+
}
|
|
460
535
|
|
|
461
|
-
|
|
536
|
+
return null
|
|
537
|
+
}),
|
|
538
|
+
)
|
|
462
539
|
}
|
|
463
540
|
|
|
464
|
-
|
|
541
|
+
private searchEffect(
|
|
465
542
|
query: string,
|
|
466
543
|
scopeId: string,
|
|
467
544
|
limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
|
|
468
545
|
memoryType?: MemoryRecord['memoryType'],
|
|
469
|
-
):
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
546
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
547
|
+
return Effect.gen(
|
|
548
|
+
function* (this: SurrealMemoryStore) {
|
|
549
|
+
aiLogger.debug`Memory store search (scopeId: ${scopeId}, memoryType: ${memoryType}, limit: ${limit})`
|
|
550
|
+
const queryEmbedding = yield* this.generateEmbeddingEffect(query)
|
|
551
|
+
const { sql, bindVars } = memoryQueryBuilder.buildVectorSearch({
|
|
552
|
+
embedding: queryEmbedding,
|
|
553
|
+
scopeId,
|
|
554
|
+
limit,
|
|
555
|
+
memoryType,
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
const results = yield* this.queryFinalStatementEffect<BasicSearchRow & { distance: number }>(sql, bindVars)
|
|
559
|
+
aiLogger.debug`Memory store search raw results: ${results.length} rows found`
|
|
560
|
+
|
|
561
|
+
const relationCounts = yield* this.fetchRelationCountsBatchEffect(results.map((row) => row.id))
|
|
562
|
+
const processed = processGraphAwareRows(
|
|
563
|
+
results,
|
|
564
|
+
relationCounts,
|
|
565
|
+
limit,
|
|
566
|
+
(row) => 1 / (1 + row.distance),
|
|
567
|
+
STRONG_GRAPH_BOOSTS,
|
|
568
|
+
MIN_RELEVANCE_SCORE,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
aiLogger.debug`Memory store search final results: ${processed.length} memories after filtering`
|
|
572
|
+
this.touchMemories(processed.map((row) => row.id))
|
|
573
|
+
return processed
|
|
574
|
+
}.bind(this),
|
|
494
575
|
)
|
|
495
|
-
|
|
496
|
-
aiLogger.debug`Memory store search final results: ${processed.length} memories after filtering`
|
|
497
|
-
this.touchMemories(processed.map((row) => row.id))
|
|
498
|
-
return processed
|
|
499
576
|
}
|
|
500
577
|
|
|
501
|
-
|
|
578
|
+
search(
|
|
502
579
|
query: string,
|
|
503
580
|
scopeId: string,
|
|
504
581
|
limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
|
|
505
582
|
memoryType?: MemoryRecord['memoryType'],
|
|
506
|
-
):
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const { sql, bindVars } = memoryQueryBuilder.buildHybridSearch({
|
|
510
|
-
query,
|
|
511
|
-
embedding: queryEmbedding,
|
|
512
|
-
scopeId,
|
|
513
|
-
limit,
|
|
514
|
-
memoryType,
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
type RrfRow = BasicSearchRow & { rrfScore: number }
|
|
518
|
-
|
|
519
|
-
const results = await this.queryFinalStatement<RrfRow>(sql, bindVars)
|
|
520
|
-
|
|
521
|
-
const memoryIds = results.map((row) => row.id)
|
|
522
|
-
const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
|
|
583
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
584
|
+
return this.searchEffect(query, scopeId, limit, memoryType)
|
|
585
|
+
}
|
|
523
586
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
587
|
+
private hybridSearchEffect(
|
|
588
|
+
query: string,
|
|
589
|
+
scopeId: string,
|
|
590
|
+
limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
|
|
591
|
+
memoryType?: MemoryRecord['memoryType'],
|
|
592
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
593
|
+
return Effect.gen(
|
|
594
|
+
function* (this: SurrealMemoryStore) {
|
|
595
|
+
const queryEmbedding = yield* this.generateEmbeddingEffect(query)
|
|
596
|
+
const { sql, bindVars } = memoryQueryBuilder.buildHybridSearch({
|
|
597
|
+
query,
|
|
598
|
+
embedding: queryEmbedding,
|
|
599
|
+
scopeId,
|
|
600
|
+
limit,
|
|
601
|
+
memoryType,
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
type RrfRow = BasicSearchRow & { rrfScore: number }
|
|
605
|
+
|
|
606
|
+
const results = yield* this.queryFinalStatementEffect<RrfRow>(sql, bindVars)
|
|
607
|
+
const relationCounts = yield* this.fetchRelationCountsBatchEffect(results.map((row) => row.id))
|
|
608
|
+
const processed = processGraphAwareRows(
|
|
609
|
+
results,
|
|
610
|
+
relationCounts,
|
|
611
|
+
limit,
|
|
612
|
+
(row) => row.rrfScore,
|
|
613
|
+
WEAK_GRAPH_BOOSTS,
|
|
614
|
+
MIN_RELEVANCE_SCORE,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
this.touchMemories(processed.map((row) => row.id))
|
|
618
|
+
return processed
|
|
619
|
+
}.bind(this),
|
|
531
620
|
)
|
|
621
|
+
}
|
|
532
622
|
|
|
533
|
-
|
|
534
|
-
|
|
623
|
+
hybridSearch(
|
|
624
|
+
query: string,
|
|
625
|
+
scopeId: string,
|
|
626
|
+
limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
|
|
627
|
+
memoryType?: MemoryRecord['memoryType'],
|
|
628
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
629
|
+
return this.hybridSearchEffect(query, scopeId, limit, memoryType)
|
|
535
630
|
}
|
|
536
631
|
|
|
537
632
|
private static HYBRID_SEARCH_TIMEOUT_MS = 2000
|
|
538
633
|
|
|
539
|
-
|
|
634
|
+
private hybridSearchWeightedEffect(
|
|
540
635
|
query: string,
|
|
541
636
|
options: {
|
|
542
637
|
scopeId: string
|
|
@@ -546,253 +641,335 @@ export class SurrealMemoryStore {
|
|
|
546
641
|
normalization?: LinearNormalization
|
|
547
642
|
fastMode?: boolean
|
|
548
643
|
},
|
|
549
|
-
):
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const tokenQuery = tokens.join(' ')
|
|
566
|
-
const fullTextQuery = tokenQuery.length > 0 ? tokenQuery : query
|
|
567
|
-
|
|
568
|
-
const weights = options.weights ?? [2, 1]
|
|
569
|
-
const normalization = options.normalization ?? 'minmax'
|
|
570
|
-
|
|
571
|
-
const { sql, bindVars } = memoryQueryBuilder.buildLinearSearch({
|
|
572
|
-
query: fullTextQuery,
|
|
573
|
-
embedding: queryEmbedding,
|
|
574
|
-
scopeId: options.scopeId,
|
|
575
|
-
limit: options.limit,
|
|
576
|
-
memoryType: options.memoryType,
|
|
577
|
-
weights,
|
|
578
|
-
normalization,
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
type LinearRow = BasicSearchRow & { linearScore: number }
|
|
644
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
645
|
+
return Effect.gen(
|
|
646
|
+
function* (this: SurrealMemoryStore) {
|
|
647
|
+
const searchStart = performance.now()
|
|
648
|
+
const queryEmbedding = yield* this.generateEmbeddingEffect(query)
|
|
649
|
+
const tokens = this.tokenizeQuery(query)
|
|
650
|
+
if (tokens.length === 0) {
|
|
651
|
+
aiLogger.debug`Skipping hybrid search (no valid tokens). Using vector search only.`
|
|
652
|
+
return yield* this.vectorSearchWithEmbeddingEffect({
|
|
653
|
+
embedding: queryEmbedding,
|
|
654
|
+
scopeId: options.scopeId,
|
|
655
|
+
limit: options.limit,
|
|
656
|
+
memoryType: options.memoryType,
|
|
657
|
+
fastMode: options.fastMode,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
582
660
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
661
|
+
const tokenQuery = tokens.join(' ')
|
|
662
|
+
const fullTextQuery = tokenQuery.length > 0 ? tokenQuery : query
|
|
663
|
+
|
|
664
|
+
const weights = options.weights ?? [2, 1]
|
|
665
|
+
const normalization = options.normalization ?? 'minmax'
|
|
666
|
+
|
|
667
|
+
const { sql, bindVars } = memoryQueryBuilder.buildLinearSearch({
|
|
668
|
+
query: fullTextQuery,
|
|
669
|
+
embedding: queryEmbedding,
|
|
670
|
+
scopeId: options.scopeId,
|
|
671
|
+
limit: options.limit,
|
|
672
|
+
memoryType: options.memoryType,
|
|
673
|
+
weights,
|
|
674
|
+
normalization,
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
type LinearRow = BasicSearchRow & { linearScore: number }
|
|
678
|
+
|
|
679
|
+
const linearResults = yield* this.queryFinalStatementEffect<LinearRow>(sql, bindVars).pipe(
|
|
680
|
+
Effect.timeout(Duration.millis(SurrealMemoryStore.HYBRID_SEARCH_TIMEOUT_MS)),
|
|
681
|
+
Effect.catchTag('TimeoutError', () =>
|
|
682
|
+
Effect.sync(() => {
|
|
683
|
+
const elapsed = performance.now() - searchStart
|
|
684
|
+
recordMemorySearchDuration(elapsed)
|
|
685
|
+
aiLogger.warn`Hybrid search timed out after ${elapsed.toFixed(0)}ms (scopeId: ${options.scopeId}). Falling back to vector-only.`
|
|
686
|
+
return null
|
|
687
|
+
}),
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if (linearResults === null) {
|
|
692
|
+
return yield* this.vectorSearchWithEmbeddingEffect({
|
|
693
|
+
embedding: queryEmbedding,
|
|
694
|
+
scopeId: options.scopeId,
|
|
695
|
+
limit: options.limit,
|
|
696
|
+
memoryType: options.memoryType,
|
|
697
|
+
fastMode: options.fastMode,
|
|
698
|
+
})
|
|
699
|
+
}
|
|
601
700
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
701
|
+
if (linearResults.length === 0) {
|
|
702
|
+
aiLogger.debug`Weighted hybrid search returned 0 raw results (scopeId: ${options.scopeId})`
|
|
703
|
+
return yield* this.fallbackWeightedSearchEffect(query, tokens, {
|
|
704
|
+
embedding: queryEmbedding,
|
|
705
|
+
scopeId: options.scopeId,
|
|
706
|
+
limit: options.limit,
|
|
707
|
+
memoryType: options.memoryType,
|
|
708
|
+
reason: 'no_raw_results',
|
|
709
|
+
fastMode: options.fastMode,
|
|
710
|
+
})
|
|
711
|
+
}
|
|
613
712
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
713
|
+
if (options.fastMode) {
|
|
714
|
+
return this.mapFastRows(linearResults, options.limit, (row) => row.linearScore)
|
|
715
|
+
}
|
|
618
716
|
|
|
619
|
-
|
|
620
|
-
|
|
717
|
+
const relationCounts = yield* this.fetchRelationCountsBatchEffect(linearResults.map((row) => row.id))
|
|
718
|
+
const processed = processGraphAwareRows(
|
|
719
|
+
linearResults,
|
|
720
|
+
relationCounts,
|
|
721
|
+
options.limit,
|
|
722
|
+
(row) => row.linearScore,
|
|
723
|
+
WEAK_GRAPH_BOOSTS,
|
|
724
|
+
MIN_RELEVANCE_SCORE,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if (processed.length === 0) {
|
|
728
|
+
aiLogger.debug`Weighted hybrid search candidates were fully filtered (scopeId: ${options.scopeId}). Falling back to vector/recent.`
|
|
729
|
+
return yield* this.fallbackWeightedSearchEffect(query, tokens, {
|
|
730
|
+
embedding: queryEmbedding,
|
|
731
|
+
scopeId: options.scopeId,
|
|
732
|
+
limit: options.limit,
|
|
733
|
+
memoryType: options.memoryType,
|
|
734
|
+
reason: 'filtered_to_zero',
|
|
735
|
+
fastMode: options.fastMode,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
621
738
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
MIN_RELEVANCE_SCORE,
|
|
739
|
+
const elapsed = performance.now() - searchStart
|
|
740
|
+
recordMemorySearchDuration(elapsed)
|
|
741
|
+
aiLogger.info`[SUCCESS_WEIGHTED_SEARCH] Weighted hybrid search succeeded (scopeId: ${options.scopeId}, rawResults: ${linearResults.length}, returned: ${processed.length}, weights: ${weights.join(',')}, normalization: ${normalization}, latencyMs: ${elapsed.toFixed(0)})`
|
|
742
|
+
this.touchMemories(processed.map((row) => row.id))
|
|
743
|
+
return processed
|
|
744
|
+
}.bind(this),
|
|
629
745
|
)
|
|
746
|
+
}
|
|
630
747
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const elapsed = performance.now() - searchStart
|
|
644
|
-
aiLogger.info`[SUCCESS_WEIGHTED_SEARCH] Weighted hybrid search succeeded (scopeId: ${options.scopeId}, rawResults: ${results.length}, returned: ${processed.length}, weights: ${weights.join(',')}, normalization: ${normalization}, latencyMs: ${elapsed.toFixed(0)})`
|
|
645
|
-
this.touchMemories(processed.map((row) => row.id))
|
|
646
|
-
return processed
|
|
748
|
+
hybridSearchWeighted(
|
|
749
|
+
query: string,
|
|
750
|
+
options: {
|
|
751
|
+
scopeId: string
|
|
752
|
+
limit: number
|
|
753
|
+
memoryType?: MemoryRecord['memoryType']
|
|
754
|
+
weights?: [number, number]
|
|
755
|
+
normalization?: LinearNormalization
|
|
756
|
+
fastMode?: boolean
|
|
757
|
+
},
|
|
758
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
759
|
+
return this.hybridSearchWeightedEffect(query, options)
|
|
647
760
|
}
|
|
648
761
|
|
|
649
|
-
|
|
762
|
+
private getEffect(id: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
|
|
650
763
|
const sql = `SELECT * FROM ${MEMORY_TABLE} WHERE id = $id`
|
|
651
|
-
|
|
652
|
-
new BoundQuery(sql, { id: ensureRecordId(id, TABLES.MEMORY) }),
|
|
764
|
+
return tryMemoryStorePromise('Failed to get memory.', () =>
|
|
765
|
+
this.db.query<SurrealMemoryRow>(new BoundQuery(sql, { id: ensureRecordId(id, TABLES.MEMORY) })),
|
|
766
|
+
).pipe(
|
|
767
|
+
Effect.map((results) => {
|
|
768
|
+
const row = results.at(0)
|
|
769
|
+
if (!row) return null
|
|
770
|
+
return mapRowToMemoryRecord(row)
|
|
771
|
+
}),
|
|
653
772
|
)
|
|
773
|
+
}
|
|
654
774
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return mapRowToMemoryRecord(row)
|
|
775
|
+
get(id: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
|
|
776
|
+
return this.getEffect(id)
|
|
658
777
|
}
|
|
659
778
|
|
|
660
|
-
|
|
779
|
+
private updateEffect(
|
|
661
780
|
id: string,
|
|
662
781
|
newContent: string,
|
|
663
782
|
options?: { importance?: number; durability?: MemoryRecord['durability']; metadata?: Record<string, unknown> },
|
|
664
|
-
):
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
await databaseService.updateWhere(MEMORY_TABLE, eq('id', ensureRecordId(id, TABLES.MEMORY)), updatePayload)
|
|
783
|
+
): Effect.Effect<void, MemoryStoreError, never> {
|
|
784
|
+
return Effect.gen(
|
|
785
|
+
function* (this: SurrealMemoryStore) {
|
|
786
|
+
const existing = yield* this.getEffect(id)
|
|
787
|
+
if (!existing) return
|
|
788
|
+
|
|
789
|
+
const newHash = hashContent(newContent, existing.scopeId, existing.memoryType)
|
|
790
|
+
const newEmbedding = yield* this.generateEmbeddingEffect(newContent)
|
|
791
|
+
const importance =
|
|
792
|
+
typeof options?.importance === 'number'
|
|
793
|
+
? Math.max(existing.importance, clampImportance(options.importance))
|
|
794
|
+
: undefined
|
|
795
|
+
|
|
796
|
+
const durability = options?.durability
|
|
797
|
+
const metadata = options?.metadata ? { ...existing.metadata, ...options.metadata } : undefined
|
|
798
|
+
|
|
799
|
+
const updatePayload: Record<string, unknown> = { content: newContent, embedding: newEmbedding, hash: newHash }
|
|
800
|
+
if (importance !== undefined) {
|
|
801
|
+
updatePayload.importance = importance
|
|
802
|
+
}
|
|
803
|
+
if (durability !== undefined) {
|
|
804
|
+
updatePayload.durability = durability
|
|
805
|
+
}
|
|
806
|
+
if (metadata !== undefined) {
|
|
807
|
+
updatePayload.metadata = metadata
|
|
808
|
+
}
|
|
691
809
|
|
|
692
|
-
|
|
810
|
+
yield* tryMemoryStorePromise('Failed to update memory.', () =>
|
|
811
|
+
this.db.updateWhere(MEMORY_TABLE, eq('id', ensureRecordId(id, TABLES.MEMORY)), updatePayload),
|
|
812
|
+
)
|
|
813
|
+
yield* this.recordHistoryEffect(id, existing.content, newContent, 'UPDATE')
|
|
814
|
+
}.bind(this),
|
|
815
|
+
)
|
|
693
816
|
}
|
|
694
817
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
818
|
+
update(
|
|
819
|
+
id: string,
|
|
820
|
+
newContent: string,
|
|
821
|
+
options?: { importance?: number; durability?: MemoryRecord['durability']; metadata?: Record<string, unknown> },
|
|
822
|
+
): Effect.Effect<void, MemoryStoreError, never> {
|
|
823
|
+
return this.updateEffect(id, newContent, options)
|
|
824
|
+
}
|
|
698
825
|
|
|
699
|
-
|
|
826
|
+
private deleteEffect(id: string): Effect.Effect<void, MemoryStoreError, never> {
|
|
827
|
+
return Effect.gen(
|
|
828
|
+
function* (this: SurrealMemoryStore) {
|
|
829
|
+
const existing = yield* this.getEffect(id)
|
|
830
|
+
if (!existing) return
|
|
700
831
|
|
|
701
|
-
|
|
832
|
+
yield* tryMemoryStorePromise('Failed to delete memory.', () => this.db.deleteById(MEMORY_TABLE, id))
|
|
833
|
+
yield* this.recordHistoryEffect(id, existing.content, null, 'DELETE')
|
|
834
|
+
}.bind(this),
|
|
835
|
+
)
|
|
702
836
|
}
|
|
703
837
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
'archivedAt IS NONE',
|
|
708
|
-
'(validUntil IS NONE OR validUntil > time::now())',
|
|
709
|
-
]
|
|
710
|
-
const bindVars: Record<string, unknown> = { scopeId: options.scopeId }
|
|
711
|
-
|
|
712
|
-
if (options.memoryType) {
|
|
713
|
-
whereClauses.push('memoryType = $memoryType')
|
|
714
|
-
bindVars.memoryType = options.memoryType
|
|
715
|
-
}
|
|
838
|
+
delete(id: string): Effect.Effect<void, MemoryStoreError, never> {
|
|
839
|
+
return this.deleteEffect(id)
|
|
840
|
+
}
|
|
716
841
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
842
|
+
private listEffect(options: MemoryListOptions): Effect.Effect<MemoryRecord[], MemoryStoreError, never> {
|
|
843
|
+
return Effect.gen(
|
|
844
|
+
function* (this: SurrealMemoryStore) {
|
|
845
|
+
const whereClauses = [
|
|
846
|
+
'scopeId = $scopeId',
|
|
847
|
+
'archivedAt IS NONE',
|
|
848
|
+
'(validUntil IS NONE OR validUntil > time::now())',
|
|
849
|
+
]
|
|
850
|
+
const bindVars: Record<string, unknown> = { scopeId: options.scopeId }
|
|
851
|
+
|
|
852
|
+
if (options.memoryType) {
|
|
853
|
+
whereClauses.push('memoryType = $memoryType')
|
|
854
|
+
bindVars.memoryType = options.memoryType
|
|
855
|
+
}
|
|
723
856
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
857
|
+
for (const [index, [key, value]] of Object.entries(options.metadataEquals ?? {}).entries()) {
|
|
858
|
+
const fieldPath = yield* this.toMetadataFieldPathEffect(key)
|
|
859
|
+
const bindKey = `metadataEquals_${index}`
|
|
860
|
+
whereClauses.push(`${fieldPath} = $${bindKey}`)
|
|
861
|
+
bindVars[bindKey] = value
|
|
862
|
+
}
|
|
730
863
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
864
|
+
for (const [index, [key, value]] of Object.entries(options.metadataNotEquals ?? {}).entries()) {
|
|
865
|
+
const fieldPath = yield* this.toMetadataFieldPathEffect(key)
|
|
866
|
+
const bindKey = `metadataNotEquals_${index}`
|
|
867
|
+
whereClauses.push(`(${fieldPath} IS NONE OR ${fieldPath} != $${bindKey})`)
|
|
868
|
+
bindVars[bindKey] = value
|
|
869
|
+
}
|
|
736
870
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
`
|
|
871
|
+
const sortDirection = options.sort === 'createdAtAsc' ? 'ASC' : 'DESC'
|
|
872
|
+
const limitClause = typeof options.limit === 'number' ? 'LIMIT $limit' : ''
|
|
873
|
+
if (typeof options.limit === 'number') {
|
|
874
|
+
bindVars.limit = options.limit
|
|
875
|
+
}
|
|
743
876
|
|
|
744
|
-
|
|
877
|
+
const sql = `
|
|
878
|
+
SELECT * FROM ${MEMORY_TABLE}
|
|
879
|
+
WHERE ${whereClauses.join('\n AND ')}
|
|
880
|
+
ORDER BY createdAt ${sortDirection}
|
|
881
|
+
${limitClause}
|
|
882
|
+
`
|
|
745
883
|
|
|
746
|
-
|
|
884
|
+
return yield* tryMemoryStorePromise('Failed to list memories.', () =>
|
|
885
|
+
this.db.query<SurrealMemoryRow>(new BoundQuery(sql, bindVars)),
|
|
886
|
+
).pipe(Effect.map((results) => results.map((row: SurrealMemoryRow) => mapRowToMemoryRecord(row))))
|
|
887
|
+
}.bind(this),
|
|
888
|
+
)
|
|
747
889
|
}
|
|
748
890
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql, { hash }))
|
|
752
|
-
|
|
753
|
-
const row = results.at(0)
|
|
754
|
-
if (!row) return null
|
|
755
|
-
return mapRowToMemoryRecord(row)
|
|
891
|
+
list(options: MemoryListOptions): Effect.Effect<MemoryRecord[], MemoryStoreError, never> {
|
|
892
|
+
return this.listEffect(options)
|
|
756
893
|
}
|
|
757
894
|
|
|
758
|
-
|
|
759
|
-
|
|
895
|
+
private addRelationEffect(
|
|
896
|
+
fromId: string,
|
|
897
|
+
toId: string,
|
|
898
|
+
relationType: RelationType,
|
|
899
|
+
confidence: number = 1.0,
|
|
900
|
+
): Effect.Effect<void, MemoryStoreError, never> {
|
|
901
|
+
const normalizedConfidence = clampImportance(confidence)
|
|
760
902
|
const fromRef = ensureRecordId(fromId, TABLES.MEMORY)
|
|
761
903
|
const toRef = ensureRecordId(toId, TABLES.MEMORY)
|
|
762
|
-
|
|
904
|
+
return Effect.gen(
|
|
905
|
+
function* (this: SurrealMemoryStore) {
|
|
906
|
+
yield* tryMemoryStorePromise('Failed to create memory relation.', () =>
|
|
907
|
+
this.db.relate(fromRef, MEMORY_RELATION_TABLE, toRef, { relationType, confidence: normalizedConfidence }),
|
|
908
|
+
)
|
|
909
|
+
if (relationType !== 'supersedes') return
|
|
910
|
+
|
|
911
|
+
yield* tryMemoryStorePromise('Failed to update superseded memory validity.', () =>
|
|
912
|
+
this.db.query(
|
|
913
|
+
new BoundQuery(
|
|
914
|
+
`UPDATE ${MEMORY_TABLE} SET validUntil = time::now() WHERE id = $toId AND validUntil IS NONE`,
|
|
915
|
+
{ toId: toRef },
|
|
916
|
+
),
|
|
917
|
+
),
|
|
918
|
+
)
|
|
919
|
+
yield* this.flagDependentsForReviewEffect(toRef)
|
|
920
|
+
}.bind(this),
|
|
921
|
+
)
|
|
922
|
+
}
|
|
763
923
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
924
|
+
addRelation(
|
|
925
|
+
fromId: string,
|
|
926
|
+
toId: string,
|
|
927
|
+
relationType: RelationType,
|
|
928
|
+
confidence: number = 1.0,
|
|
929
|
+
): Effect.Effect<void, MemoryStoreError, never> {
|
|
930
|
+
return this.addRelationEffect(fromId, toId, relationType, confidence)
|
|
772
931
|
}
|
|
773
932
|
|
|
774
|
-
private
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
933
|
+
private flagDependentsForReviewEffect(supersededId: RecordIdRef): Effect.Effect<void, MemoryStoreError, never> {
|
|
934
|
+
return tryMemoryStorePromise('Failed to flag dependent memories for review.', () =>
|
|
935
|
+
this.db.query<{ id: RecordIdInput }>(
|
|
936
|
+
new BoundQuery(
|
|
937
|
+
`SELECT id FROM ${MEMORY_TABLE}
|
|
938
|
+
WHERE ->${MEMORY_RELATION_TABLE}[WHERE relationType = 'depends_on']->${MEMORY_TABLE} CONTAINS $supersededId
|
|
939
|
+
AND archivedAt IS NONE
|
|
940
|
+
AND needsReview = false`,
|
|
941
|
+
{ supersededId },
|
|
942
|
+
),
|
|
782
943
|
),
|
|
944
|
+
).pipe(
|
|
945
|
+
Effect.flatMap((dependents) => {
|
|
946
|
+
if (dependents.length === 0) return Effect.void
|
|
947
|
+
|
|
948
|
+
const ids = dependents.map((d: { id: RecordIdInput }) => ensureRecordId(d.id, TABLES.MEMORY))
|
|
949
|
+
return tryMemoryStorePromise('Failed to flag dependent memories for review.', () =>
|
|
950
|
+
this.db.updateWhere(MEMORY_TABLE, inside('id', ids), { needsReview: true }),
|
|
951
|
+
).pipe(
|
|
952
|
+
Effect.tap(() =>
|
|
953
|
+
Effect.sync(() => {
|
|
954
|
+
aiLogger.debug`Flagged ${dependents.length} dependent memories for review after supersede`
|
|
955
|
+
}),
|
|
956
|
+
),
|
|
957
|
+
)
|
|
958
|
+
}),
|
|
783
959
|
)
|
|
960
|
+
}
|
|
784
961
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
962
|
+
getRelatedMemories(
|
|
963
|
+
memoryId: string,
|
|
964
|
+
relationType?: RelationType,
|
|
965
|
+
): Effect.Effect<{ relatesTo: MemoryRecord[]; relatedBy: MemoryRecord[] }, MemoryStoreError, never> {
|
|
966
|
+
return this.getRelatedMemoriesEffect(memoryId, relationType)
|
|
790
967
|
}
|
|
791
968
|
|
|
792
|
-
|
|
969
|
+
private getRelatedMemoriesEffect(
|
|
793
970
|
memoryId: string,
|
|
794
971
|
relationType?: RelationType,
|
|
795
|
-
):
|
|
972
|
+
): Effect.Effect<{ relatesTo: MemoryRecord[]; relatedBy: MemoryRecord[] }, MemoryStoreError, never> {
|
|
796
973
|
const typeFilter = relationType ? `[WHERE relationType = $relationType]` : ''
|
|
797
974
|
|
|
798
975
|
const sql = `
|
|
@@ -802,18 +979,30 @@ export class SurrealMemoryStore {
|
|
|
802
979
|
FROM ONLY $memoryId
|
|
803
980
|
`
|
|
804
981
|
|
|
805
|
-
|
|
806
|
-
|
|
982
|
+
return tryMemoryStorePromise('Failed to get related memories.', () =>
|
|
983
|
+
this.db.query<{ relatesTo: SurrealMemoryRow[]; relatedBy: SurrealMemoryRow[] }>(
|
|
984
|
+
new BoundQuery(sql, { memoryId: ensureRecordId(memoryId, TABLES.MEMORY), relationType }),
|
|
985
|
+
),
|
|
986
|
+
).pipe(
|
|
987
|
+
Effect.map((result) => {
|
|
988
|
+
const data = result[0] ?? { relatesTo: [], relatedBy: [] }
|
|
989
|
+
return {
|
|
990
|
+
relatesTo: data.relatesTo.map((row: SurrealMemoryRow) => mapRowToMemoryRecord(row)),
|
|
991
|
+
relatedBy: data.relatedBy.map((row: SurrealMemoryRow) => mapRowToMemoryRecord(row)),
|
|
992
|
+
}
|
|
993
|
+
}),
|
|
807
994
|
)
|
|
995
|
+
}
|
|
808
996
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
997
|
+
findConflicts(
|
|
998
|
+
scopeId: string,
|
|
999
|
+
): Effect.Effect<Array<{ memory: MemoryRecord; contradictedBy: MemoryRecord[] }>, MemoryStoreError, never> {
|
|
1000
|
+
return this.findConflictsEffect(scopeId)
|
|
814
1001
|
}
|
|
815
1002
|
|
|
816
|
-
|
|
1003
|
+
private findConflictsEffect(
|
|
1004
|
+
scopeId: string,
|
|
1005
|
+
): Effect.Effect<Array<{ memory: MemoryRecord; contradictedBy: MemoryRecord[] }>, MemoryStoreError, never> {
|
|
817
1006
|
const sql = `
|
|
818
1007
|
SELECT
|
|
819
1008
|
*,
|
|
@@ -823,91 +1012,132 @@ export class SurrealMemoryStore {
|
|
|
823
1012
|
AND count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'contradicts']) > 0
|
|
824
1013
|
`
|
|
825
1014
|
|
|
826
|
-
|
|
827
|
-
new BoundQuery(sql, { scopeId }),
|
|
1015
|
+
return tryMemoryStorePromise('Failed to find memory conflicts.', () =>
|
|
1016
|
+
this.db.query<SurrealMemoryRow & { contradictedBy: SurrealMemoryRow[] }>(new BoundQuery(sql, { scopeId })),
|
|
1017
|
+
).pipe(
|
|
1018
|
+
Effect.map((results) =>
|
|
1019
|
+
results.map((row: SurrealMemoryRow & { contradictedBy: SurrealMemoryRow[] }) => ({
|
|
1020
|
+
memory: mapRowToMemoryRecord(row),
|
|
1021
|
+
contradictedBy: row.contradictedBy.map((r: SurrealMemoryRow) => mapRowToMemoryRecord(r)),
|
|
1022
|
+
})),
|
|
1023
|
+
),
|
|
828
1024
|
)
|
|
829
|
-
|
|
830
|
-
return results.map((row) => ({
|
|
831
|
-
memory: mapRowToMemoryRecord(row),
|
|
832
|
-
contradictedBy: row.contradictedBy.map((r) => mapRowToMemoryRecord(r)),
|
|
833
|
-
}))
|
|
834
1025
|
}
|
|
835
1026
|
|
|
836
|
-
|
|
1027
|
+
private graphWalkEffect(
|
|
837
1028
|
startId: string,
|
|
838
1029
|
depth = 2,
|
|
839
|
-
):
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1030
|
+
): Effect.Effect<
|
|
1031
|
+
{
|
|
1032
|
+
memories: MemoryRecord[]
|
|
1033
|
+
edges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }>
|
|
1034
|
+
},
|
|
1035
|
+
MemoryStoreError,
|
|
1036
|
+
never
|
|
1037
|
+
> {
|
|
843
1038
|
const maxDepth = Math.min(depth, 3)
|
|
844
1039
|
|
|
845
1040
|
const visited = new Set<string>([startId])
|
|
846
1041
|
const allEdges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }> = []
|
|
847
1042
|
const allMemories: MemoryRecord[] = []
|
|
848
1043
|
|
|
849
|
-
|
|
1044
|
+
const walkHop = (frontier: string[], hop: number): Effect.Effect<void, MemoryStoreError, never> => {
|
|
1045
|
+
if (hop >= maxDepth || frontier.length === 0) {
|
|
1046
|
+
return Effect.void
|
|
1047
|
+
}
|
|
850
1048
|
|
|
851
|
-
for (let hop = 0; hop < maxDepth && frontier.length > 0; hop++) {
|
|
852
1049
|
const nextFrontier: string[] = []
|
|
853
1050
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1051
|
+
return Effect.all(
|
|
1052
|
+
frontier.map((nodeId) =>
|
|
1053
|
+
tryMemoryStorePromise('Failed to walk memory graph.', () =>
|
|
1054
|
+
this.db.query<{
|
|
1055
|
+
outEdges: Array<{
|
|
1056
|
+
from: RecordIdInput
|
|
1057
|
+
to: RecordIdInput
|
|
1058
|
+
relationType: RelationType
|
|
1059
|
+
confidence: number
|
|
1060
|
+
}>
|
|
1061
|
+
outMemories: SurrealMemoryRow[]
|
|
1062
|
+
inEdges: Array<{ from: RecordIdInput; to: RecordIdInput; relationType: RelationType; confidence: number }>
|
|
1063
|
+
inMemories: SurrealMemoryRow[]
|
|
1064
|
+
}>(
|
|
1065
|
+
new BoundQuery(
|
|
1066
|
+
`
|
|
1067
|
+
SELECT
|
|
1068
|
+
->${MEMORY_RELATION_TABLE}.{in AS from, out AS to, relationType, confidence} AS outEdges,
|
|
1069
|
+
->${MEMORY_RELATION_TABLE}->${MEMORY_TABLE}[WHERE archivedAt IS NONE].* AS outMemories,
|
|
1070
|
+
<-${MEMORY_RELATION_TABLE}.{in AS from, out AS to, relationType, confidence} AS inEdges,
|
|
1071
|
+
<-${MEMORY_RELATION_TABLE}<-${MEMORY_TABLE}[WHERE archivedAt IS NONE].* AS inMemories
|
|
1072
|
+
FROM ONLY $nodeId
|
|
1073
|
+
`,
|
|
1074
|
+
{ nodeId: ensureRecordId(nodeId, TABLES.MEMORY) },
|
|
1075
|
+
),
|
|
1076
|
+
),
|
|
1077
|
+
),
|
|
1078
|
+
),
|
|
1079
|
+
).pipe(
|
|
1080
|
+
Effect.flatMap((results) =>
|
|
1081
|
+
Effect.sync(() => {
|
|
1082
|
+
for (const result of results) {
|
|
1083
|
+
const row = result.at(0)
|
|
1084
|
+
if (!row) continue
|
|
1085
|
+
|
|
1086
|
+
for (const edge of row.outEdges) {
|
|
1087
|
+
allEdges.push({
|
|
1088
|
+
from: recordIdToString(edge.from, TABLES.MEMORY),
|
|
1089
|
+
to: recordIdToString(edge.to, TABLES.MEMORY),
|
|
1090
|
+
relationType: edge.relationType,
|
|
1091
|
+
confidence: edge.confidence,
|
|
1092
|
+
})
|
|
1093
|
+
}
|
|
1094
|
+
for (const edge of row.inEdges) {
|
|
1095
|
+
allEdges.push({
|
|
1096
|
+
from: recordIdToString(edge.from, TABLES.MEMORY),
|
|
1097
|
+
to: recordIdToString(edge.to, TABLES.MEMORY),
|
|
1098
|
+
relationType: edge.relationType,
|
|
1099
|
+
confidence: edge.confidence,
|
|
1100
|
+
})
|
|
1101
|
+
}
|
|
1102
|
+
for (const mem of [...row.outMemories, ...row.inMemories]) {
|
|
1103
|
+
const memoryId = recordIdToString(mem.id, TABLES.MEMORY)
|
|
1104
|
+
if (!visited.has(memoryId)) {
|
|
1105
|
+
visited.add(memoryId)
|
|
1106
|
+
allMemories.push(mapRowToMemoryRecord(mem))
|
|
1107
|
+
nextFrontier.push(memoryId)
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
894
1110
|
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
1111
|
|
|
899
|
-
|
|
1112
|
+
return undefined
|
|
1113
|
+
}).pipe(Effect.flatMap(() => walkHop(nextFrontier, hop + 1))),
|
|
1114
|
+
),
|
|
1115
|
+
)
|
|
900
1116
|
}
|
|
901
1117
|
|
|
902
|
-
return { memories: allMemories, edges: allEdges }
|
|
1118
|
+
return walkHop([startId], 0).pipe(Effect.as({ memories: allMemories, edges: allEdges }))
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
graphWalk(
|
|
1122
|
+
startId: string,
|
|
1123
|
+
depth = 2,
|
|
1124
|
+
): Effect.Effect<
|
|
1125
|
+
{
|
|
1126
|
+
memories: MemoryRecord[]
|
|
1127
|
+
edges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }>
|
|
1128
|
+
},
|
|
1129
|
+
MemoryStoreError,
|
|
1130
|
+
never
|
|
1131
|
+
> {
|
|
1132
|
+
return this.graphWalkEffect(startId, depth)
|
|
903
1133
|
}
|
|
904
1134
|
|
|
905
|
-
private
|
|
1135
|
+
private recordHistoryEffect(
|
|
906
1136
|
memoryId: string,
|
|
907
1137
|
prevValue: string | null,
|
|
908
1138
|
newValue: string | null,
|
|
909
1139
|
event: MemoryEvent,
|
|
910
|
-
):
|
|
1140
|
+
): Effect.Effect<void, MemoryStoreError, never> {
|
|
911
1141
|
const memoryRef = ensureRecordId(memoryId, TABLES.MEMORY)
|
|
912
1142
|
const historyRow: Record<string, unknown> = {
|
|
913
1143
|
memoryId: memoryRef,
|
|
@@ -915,12 +1145,17 @@ export class SurrealMemoryStore {
|
|
|
915
1145
|
...(prevValue === null ? {} : { prevValue }),
|
|
916
1146
|
...(newValue === null ? {} : { newValue }),
|
|
917
1147
|
}
|
|
918
|
-
|
|
1148
|
+
return tryMemoryStorePromise('Failed to record memory history.', () =>
|
|
1149
|
+
this.db.insert<Record<string, unknown>>(MEMORY_HISTORY_TABLE, historyRow),
|
|
1150
|
+
).pipe(Effect.asVoid)
|
|
919
1151
|
}
|
|
920
1152
|
|
|
921
|
-
|
|
1153
|
+
private enrichWithNeighborsEffect(
|
|
1154
|
+
results: MemorySearchResult[],
|
|
1155
|
+
topN: number = 5,
|
|
1156
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
922
1157
|
const topIds = results.slice(0, topN).map((r) => r.id)
|
|
923
|
-
if (topIds.length === 0) return results
|
|
1158
|
+
if (topIds.length === 0) return Effect.succeed(results)
|
|
924
1159
|
|
|
925
1160
|
const topRefs = topIds.map((id) => ensureRecordId(id, TABLES.MEMORY))
|
|
926
1161
|
|
|
@@ -933,65 +1168,88 @@ export class SurrealMemoryStore {
|
|
|
933
1168
|
WHERE id IN $ids
|
|
934
1169
|
`
|
|
935
1170
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1171
|
+
return tryMemoryStorePromise('Failed to enrich memories with neighbors.', () =>
|
|
1172
|
+
this.db.query<{
|
|
1173
|
+
id: RecordIdInput
|
|
1174
|
+
outgoing: Array<{ id: RecordIdInput; content: string; relationType?: string }> | null
|
|
1175
|
+
incoming: Array<{ id: RecordIdInput; content: string; relationType?: string }> | null
|
|
1176
|
+
}>(new BoundQuery(sql, { ids: topRefs })),
|
|
1177
|
+
).pipe(
|
|
1178
|
+
Effect.map((rows) => {
|
|
1179
|
+
const neighborMap = new Map<string, string[]>()
|
|
1180
|
+
for (const row of rows) {
|
|
1181
|
+
const rowId = recordIdToString(row.id, TABLES.MEMORY)
|
|
1182
|
+
const contexts: string[] = []
|
|
1183
|
+
const seen = new Set<string>()
|
|
1184
|
+
for (const neighbor of [...(row.outgoing ?? []), ...(row.incoming ?? [])]) {
|
|
1185
|
+
const neighborId = recordIdToString(neighbor.id, TABLES.MEMORY)
|
|
1186
|
+
if (!neighbor.content || seen.has(neighborId)) continue
|
|
1187
|
+
seen.add(neighborId)
|
|
1188
|
+
const label = neighbor.relationType ? `[${neighbor.relationType}]` : ''
|
|
1189
|
+
const truncated = truncateText(neighbor.content, 200)
|
|
1190
|
+
contexts.push(`${label} ${truncated}`.trim())
|
|
1191
|
+
}
|
|
1192
|
+
if (contexts.length > 0) {
|
|
1193
|
+
neighborMap.set(rowId, contexts)
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
959
1196
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1197
|
+
return results.map((result) => {
|
|
1198
|
+
const neighbors = neighborMap.get(result.id)
|
|
1199
|
+
if (!neighbors) return result
|
|
1200
|
+
return { ...result, metadata: { ...result.metadata, relatedContext: neighbors } }
|
|
1201
|
+
})
|
|
1202
|
+
}),
|
|
1203
|
+
)
|
|
965
1204
|
}
|
|
966
1205
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1206
|
+
enrichWithNeighbors(
|
|
1207
|
+
results: MemorySearchResult[],
|
|
1208
|
+
topN: number = 5,
|
|
1209
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
1210
|
+
return this.enrichWithNeighborsEffect(results, topN)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private getStaleMemoriesEffect(
|
|
1214
|
+
scopeId: string,
|
|
1215
|
+
limit: number = 5,
|
|
1216
|
+
): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
1217
|
+
return tryMemoryStorePromise('Failed to list stale memories.', () =>
|
|
1218
|
+
this.db.query<BasicSearchRow>(
|
|
1219
|
+
new BoundQuery(
|
|
1220
|
+
`SELECT id, content, metadata
|
|
1221
|
+
FROM ${MEMORY_TABLE}
|
|
1222
|
+
WHERE scopeId = $scopeId
|
|
1223
|
+
AND needsReview = true
|
|
1224
|
+
AND archivedAt IS NONE
|
|
1225
|
+
ORDER BY updatedAt DESC
|
|
1226
|
+
LIMIT $limit`,
|
|
1227
|
+
{ scopeId, limit },
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1230
|
+
).pipe(
|
|
1231
|
+
Effect.map((results) =>
|
|
1232
|
+
results.map((row: BasicSearchRow) => ({
|
|
1233
|
+
id: recordIdToString(row.id, TABLES.MEMORY),
|
|
1234
|
+
content: row.content,
|
|
1235
|
+
score: 0,
|
|
1236
|
+
metadata: { ...row.metadata, needsReview: true },
|
|
1237
|
+
})),
|
|
978
1238
|
),
|
|
979
1239
|
)
|
|
1240
|
+
}
|
|
980
1241
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
content: row.content,
|
|
984
|
-
score: 0,
|
|
985
|
-
metadata: { ...row.metadata, needsReview: true },
|
|
986
|
-
}))
|
|
1242
|
+
getStaleMemories(scopeId: string, limit: number = 5): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
|
|
1243
|
+
return this.getStaleMemoriesEffect(scopeId, limit)
|
|
987
1244
|
}
|
|
988
1245
|
}
|
|
989
1246
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1247
|
+
export function createMemoryStore(
|
|
1248
|
+
db: SurrealDBService,
|
|
1249
|
+
options: { embeddingModel: string; openRouterApiKey?: string },
|
|
1250
|
+
): SurrealMemoryStore {
|
|
1251
|
+
return new SurrealMemoryStore(
|
|
1252
|
+
db,
|
|
1253
|
+
new ProviderEmbeddings({ modelId: options.embeddingModel, openRouterApiKey: options.openRouterApiKey }),
|
|
1254
|
+
)
|
|
997
1255
|
}
|