@lota-sdk/core 0.4.8 → 0.4.10

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