@lota-sdk/core 0.4.8 → 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.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/thread-defaults.ts +33 -21
  9. package/src/create-runtime.ts +725 -387
  10. package/src/db/base.service.ts +52 -28
  11. package/src/db/cursor-pagination.ts +71 -30
  12. package/src/db/memory-store.helpers.ts +4 -7
  13. package/src/db/memory-store.ts +856 -598
  14. package/src/db/memory.ts +398 -275
  15. package/src/db/record-id.ts +32 -10
  16. package/src/db/schema-fingerprint.ts +30 -12
  17. package/src/db/service-normalization.ts +255 -0
  18. package/src/db/service.ts +726 -761
  19. package/src/db/startup.ts +140 -66
  20. package/src/db/transaction-conflict.ts +15 -0
  21. package/src/effect/awaitable-effect.ts +87 -0
  22. package/src/effect/errors.ts +121 -0
  23. package/src/effect/helpers.ts +98 -0
  24. package/src/effect/index.ts +22 -0
  25. package/src/effect/layers.ts +228 -0
  26. package/src/effect/runtime-ref.ts +25 -0
  27. package/src/effect/runtime.ts +31 -0
  28. package/src/effect/services.ts +57 -0
  29. package/src/effect/zod.ts +43 -0
  30. package/src/embeddings/provider.ts +122 -76
  31. package/src/index.ts +46 -1
  32. package/src/openrouter/direct-provider.ts +11 -35
  33. package/src/queues/autonomous-job.queue.ts +130 -74
  34. package/src/queues/context-compaction.queue.ts +60 -15
  35. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  36. package/src/queues/document-processor.queue.ts +52 -77
  37. package/src/queues/memory-consolidation.queue.ts +47 -32
  38. package/src/queues/organization-learning.queue.ts +13 -4
  39. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  40. package/src/queues/plan-scheduler.queue.ts +107 -31
  41. package/src/queues/post-chat-memory.queue.ts +66 -24
  42. package/src/queues/queue-factory.ts +142 -52
  43. package/src/queues/standalone-worker.ts +39 -0
  44. package/src/queues/title-generation.queue.ts +54 -9
  45. package/src/redis/connection.ts +84 -32
  46. package/src/redis/index.ts +6 -8
  47. package/src/redis/org-memory-lock.ts +60 -27
  48. package/src/redis/redis-lease-lock.ts +200 -121
  49. package/src/redis/runtime-connection.ts +10 -0
  50. package/src/redis/stream-context.ts +84 -46
  51. package/src/runtime/agent-identity-overrides.ts +2 -2
  52. package/src/runtime/agent-runtime-policy.ts +4 -1
  53. package/src/runtime/agent-stream-helpers.ts +20 -9
  54. package/src/runtime/chat-run-orchestration.ts +102 -19
  55. package/src/runtime/chat-run-registry.ts +36 -2
  56. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  57. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  58. package/src/runtime/execution-plan-visibility.ts +2 -2
  59. package/src/runtime/execution-plan.ts +42 -15
  60. package/src/runtime/graph-designer.ts +11 -7
  61. package/src/runtime/helper-model.ts +135 -48
  62. package/src/runtime/index.ts +7 -7
  63. package/src/runtime/indexed-repositories-policy.ts +3 -3
  64. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  65. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  66. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  67. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  68. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  69. package/src/runtime/plugin-resolution.ts +144 -24
  70. package/src/runtime/plugin-types.ts +9 -1
  71. package/src/runtime/post-turn-side-effects.ts +197 -130
  72. package/src/runtime/retrieval-adapters.ts +38 -4
  73. package/src/runtime/runtime-config.ts +150 -61
  74. package/src/runtime/runtime-extensions.ts +21 -34
  75. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  76. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  77. package/src/runtime/social-chat/social-chat.ts +594 -0
  78. package/src/runtime/specialist-runner.ts +36 -10
  79. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  80. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  81. package/src/runtime/thread-chat-helpers.ts +2 -2
  82. package/src/runtime/thread-plan-turn.ts +2 -1
  83. package/src/runtime/thread-turn-context.ts +172 -94
  84. package/src/runtime/turn-lifecycle.ts +93 -27
  85. package/src/services/agent-activity.service.ts +287 -203
  86. package/src/services/agent-executor.service.ts +329 -217
  87. package/src/services/artifact.service.ts +225 -148
  88. package/src/services/attachment.service.ts +137 -115
  89. package/src/services/autonomous-job.service.ts +888 -491
  90. package/src/services/chat-run-registry.service.ts +11 -1
  91. package/src/services/context-compaction.service.ts +136 -86
  92. package/src/services/document-chunk.service.ts +162 -90
  93. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  94. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  95. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  96. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  97. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  98. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  99. package/src/services/feedback-loop.service.ts +132 -76
  100. package/src/services/global-orchestrator.service.ts +80 -170
  101. package/src/services/graph-full-routing.ts +182 -0
  102. package/src/services/index.ts +18 -21
  103. package/src/services/institutional-memory.service.ts +220 -123
  104. package/src/services/learned-skill.service.ts +364 -259
  105. package/src/services/memory/memory-conversation.ts +95 -0
  106. package/src/services/memory/memory-org-memory.ts +39 -0
  107. package/src/services/memory/memory-preseeded.ts +80 -0
  108. package/src/services/memory/memory-rerank.ts +297 -0
  109. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  110. package/src/services/memory/memory.service.ts +692 -0
  111. package/src/services/memory/rerank.service.ts +209 -0
  112. package/src/services/monitoring-window.service.ts +92 -70
  113. package/src/services/mutating-approval.service.ts +62 -53
  114. package/src/services/node-workspace.service.ts +141 -98
  115. package/src/services/notification.service.ts +17 -16
  116. package/src/services/organization-member.service.ts +120 -66
  117. package/src/services/organization.service.ts +144 -51
  118. package/src/services/ownership-dispatcher.service.ts +415 -264
  119. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  120. package/src/services/plan/plan-agent-query.service.ts +322 -0
  121. package/src/services/plan/plan-approval.service.ts +102 -0
  122. package/src/services/plan/plan-artifact.service.ts +60 -0
  123. package/src/services/plan/plan-builder.service.ts +76 -0
  124. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  125. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  126. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  127. package/src/services/plan/plan-coordination.service.ts +181 -0
  128. package/src/services/plan/plan-cycle.service.ts +398 -0
  129. package/src/services/plan/plan-deadline.service.ts +547 -0
  130. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  131. package/src/services/plan/plan-executor-context.ts +35 -0
  132. package/src/services/plan/plan-executor-graph.ts +475 -0
  133. package/src/services/plan/plan-executor-helpers.ts +322 -0
  134. package/src/services/plan/plan-executor-persistence.ts +209 -0
  135. package/src/services/plan/plan-executor.service.ts +1654 -0
  136. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  137. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  138. package/src/services/plan/plan-run-serialization.ts +15 -0
  139. package/src/services/plan/plan-run.service.ts +644 -0
  140. package/src/services/plan/plan-scheduler.service.ts +385 -0
  141. package/src/services/plan/plan-template.service.ts +224 -0
  142. package/src/services/plan/plan-transaction-events.ts +33 -0
  143. package/src/services/plan/plan-validator.service.ts +907 -0
  144. package/src/services/plan/plan-workspace.service.ts +125 -0
  145. package/src/services/plugin-executor.service.ts +97 -68
  146. package/src/services/quality-metrics.service.ts +112 -94
  147. package/src/services/queue-job.service.ts +296 -230
  148. package/src/services/recent-activity-title.service.ts +65 -36
  149. package/src/services/recent-activity.service.ts +274 -259
  150. package/src/services/skill-resolver.service.ts +38 -12
  151. package/src/services/social-chat-history.service.ts +176 -125
  152. package/src/services/system-executor.service.ts +91 -61
  153. package/src/services/thread/thread-active-run.ts +203 -0
  154. package/src/services/thread/thread-bootstrap.ts +369 -0
  155. package/src/services/thread/thread-listing.ts +198 -0
  156. package/src/services/thread/thread-memory-block.ts +117 -0
  157. package/src/services/thread/thread-message.service.ts +363 -0
  158. package/src/services/thread/thread-record-store.ts +155 -0
  159. package/src/services/thread/thread-title.service.ts +74 -0
  160. package/src/services/thread/thread-turn-execution.ts +280 -0
  161. package/src/services/thread/thread-turn-message-context.ts +73 -0
  162. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  163. package/src/services/thread/thread-turn-streaming.ts +402 -0
  164. package/src/services/thread/thread-turn-tracing.ts +35 -0
  165. package/src/services/thread/thread-turn.ts +343 -0
  166. package/src/services/thread/thread.service.ts +335 -0
  167. package/src/services/user.service.ts +82 -32
  168. package/src/services/write-intent-validator.service.ts +63 -51
  169. package/src/storage/attachment-parser.ts +69 -27
  170. package/src/storage/attachment-storage.service.ts +331 -275
  171. package/src/storage/generated-document-storage.service.ts +66 -34
  172. package/src/system-agents/agent-result.ts +3 -1
  173. package/src/system-agents/context-compaction.agent.ts +2 -2
  174. package/src/system-agents/delegated-agent-factory.ts +159 -90
  175. package/src/system-agents/memory-reranker.agent.ts +2 -2
  176. package/src/system-agents/memory.agent.ts +2 -2
  177. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  178. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  179. package/src/system-agents/skill-extractor.agent.ts +2 -2
  180. package/src/system-agents/skill-manager.agent.ts +2 -2
  181. package/src/system-agents/thread-router.agent.ts +157 -113
  182. package/src/system-agents/title-generator.agent.ts +2 -2
  183. package/src/tools/execution-plan.tool.ts +220 -161
  184. package/src/tools/fetch-webpage.tool.ts +21 -17
  185. package/src/tools/firecrawl-client.ts +16 -6
  186. package/src/tools/index.ts +1 -0
  187. package/src/tools/memory-block.tool.ts +14 -6
  188. package/src/tools/plan-approval.tool.ts +49 -47
  189. package/src/tools/read-file-parts.tool.ts +44 -33
  190. package/src/tools/remember-memory.tool.ts +65 -45
  191. package/src/tools/search-web.tool.ts +26 -22
  192. package/src/tools/search.tool.ts +41 -29
  193. package/src/tools/team-think.tool.ts +124 -83
  194. package/src/tools/user-questions.tool.ts +4 -3
  195. package/src/tools/web-tool-shared.ts +6 -0
  196. package/src/utils/async.ts +17 -23
  197. package/src/utils/crypto.ts +21 -0
  198. package/src/utils/date-time.ts +40 -1
  199. package/src/utils/errors.ts +95 -16
  200. package/src/utils/hono-error-handler.ts +24 -39
  201. package/src/utils/index.ts +2 -1
  202. package/src/utils/null-proto-record.ts +41 -0
  203. package/src/utils/sse-keepalive.ts +124 -21
  204. package/src/workers/bootstrap.ts +186 -51
  205. package/src/workers/memory-consolidation.worker.ts +325 -237
  206. package/src/workers/organization-learning.worker.ts +50 -16
  207. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  208. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  209. package/src/workers/skill-extraction.runner.ts +176 -93
  210. package/src/workers/utils/file-section-chunker.ts +8 -10
  211. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  212. package/src/workers/utils/repomix-file-sections.ts +2 -2
  213. package/src/workers/utils/thread-message-query.ts +97 -38
  214. package/src/workers/worker-utils.ts +56 -31
  215. package/src/config/debug-logger.ts +0 -47
  216. package/src/redis/connection-accessor.ts +0 -26
  217. package/src/runtime/context-compaction-runtime.ts +0 -87
  218. package/src/runtime/social-chat-agent-runner.ts +0 -118
  219. package/src/runtime/social-chat.ts +0 -516
  220. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  221. package/src/services/adaptive-playbook.service.ts +0 -152
  222. package/src/services/artifact-provenance.service.ts +0 -172
  223. package/src/services/chat-attachments.service.ts +0 -17
  224. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  225. package/src/services/execution-plan.service.ts +0 -1118
  226. package/src/services/memory.service.ts +0 -914
  227. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  228. package/src/services/plan-agent-query.service.ts +0 -267
  229. package/src/services/plan-approval.service.ts +0 -83
  230. package/src/services/plan-artifact.service.ts +0 -50
  231. package/src/services/plan-builder.service.ts +0 -67
  232. package/src/services/plan-checkpoint.service.ts +0 -81
  233. package/src/services/plan-completion-side-effects.ts +0 -80
  234. package/src/services/plan-coordination.service.ts +0 -157
  235. package/src/services/plan-cycle.service.ts +0 -284
  236. package/src/services/plan-deadline.service.ts +0 -430
  237. package/src/services/plan-event-delivery.service.ts +0 -166
  238. package/src/services/plan-executor.service.ts +0 -1950
  239. package/src/services/plan-run.service.ts +0 -515
  240. package/src/services/plan-scheduler.service.ts +0 -240
  241. package/src/services/plan-template.service.ts +0 -177
  242. package/src/services/plan-validator.service.ts +0 -818
  243. package/src/services/plan-workspace.service.ts +0 -83
  244. package/src/services/rerank.service.ts +0 -156
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -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 { getDefaultEmbeddings } from '../embeddings/provider'
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 { databaseService } from './service'
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 TOUCH_MEMORIES_MAX_ATTEMPTS = 4
34
- const TOUCH_MEMORIES_RETRY_BASE_DELAY_MS = 25
35
- const TOUCH_MEMORIES_RETRY_JITTER_MS = 20
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
- constructor(private embeddings: EmbeddingClient) {}
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 toMetadataFieldPath(key: string): string {
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
- throw new Error(`Invalid memory metadata filter key: ${key}`)
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 async listRecentBasic(options: {
123
+ private listRecentBasic(options: {
83
124
  scopeId: string
84
125
  limit: number
85
126
  memoryType?: MemoryRecord['memoryType']
86
- }): Promise<BasicSearchRow[]> {
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 databaseService.query<BasicSearchRow>(
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
- async listTopMemories(options: {
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
- }): Promise<MemoryRecord[]> {
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
- 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
- )
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
- return rows.map((row) => mapRowToMemoryRecord(row))
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 async vectorSearchWithEmbedding(options: {
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
- }): Promise<MemorySearchResult[]> {
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
- 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)
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
- const processed = processGraphAwareRows(
158
- results,
159
- relationCounts,
160
- options.limit,
161
- (row) => 1 / (1 + row.distance),
162
- STRONG_GRAPH_BOOSTS,
163
- MIN_RELEVANCE_SCORE,
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 async generateEmbedding(content: string): Promise<number[]> {
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
- async warmEmbedding(content: string): Promise<void> {
193
- await this.generateEmbedding(content)
245
+ warmEmbedding(content: string): Effect.Effect<void, MemoryStoreError, never> {
246
+ return Effect.asVoid(this.generateEmbeddingEffect(content))
194
247
  }
195
248
 
196
- private async fetchRelationCountsBatch(memoryIds: RecordIdInput[]): Promise<Map<string, RelationCounts>> {
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
- 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
- }
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
- return countsMap
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 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
- }
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 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[]) : []
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 async fallbackWeightedSearch(
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
- ): 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
- }
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
- 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,
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
- async insert(
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
- ): Promise<string> {
405
+ ): Effect.Effect<string, MemoryStoreError, never> {
357
406
  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) : ''
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
- await this.recordHistory(id, null, content, 'ADD')
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
- return id
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
- 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 },
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
- const row = rows.at(0)
404
- return row ? mapRowToMemoryRecord(row) : null
473
+ getByHash(hash: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
474
+ return this.getByHashEffect(hash)
405
475
  }
406
476
 
407
- private async findNearDuplicate(
477
+ private findNearDuplicateEffect(
408
478
  embedding: number[],
409
479
  scopeId: string,
410
480
  content: string,
411
- ): Promise<{ id: string; content: string; importance: number; similarity: number } | null> {
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
- const neighborRows = await this.queryFinalStatement<{
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
- 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
- }
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
- return null
536
+ return null
537
+ }),
538
+ )
462
539
  }
463
540
 
464
- async search(
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
- ): 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,
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
- async hybridSearch(
578
+ search(
502
579
  query: string,
503
580
  scopeId: string,
504
581
  limit: number = DEFAULT_MEMORY_SEARCH_LIMIT,
505
582
  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)
583
+ ): Effect.Effect<MemorySearchResult[], MemoryStoreError, never> {
584
+ return this.searchEffect(query, scopeId, limit, memoryType)
585
+ }
523
586
 
524
- const processed = processGraphAwareRows(
525
- results,
526
- relationCounts,
527
- limit,
528
- (row) => row.rrfScore,
529
- WEAK_GRAPH_BOOSTS,
530
- MIN_RELEVANCE_SCORE,
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
- this.touchMemories(processed.map((row) => row.id))
534
- return processed
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
- async hybridSearchWeighted(
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
- ): 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 }
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
- 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
- }
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
- 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
- }
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
- if (options.fastMode) {
615
- const fastResults = this.mapFastRows(results, options.limit, (row) => row.linearScore)
616
- return fastResults
617
- }
713
+ if (options.fastMode) {
714
+ return this.mapFastRows(linearResults, options.limit, (row) => row.linearScore)
715
+ }
618
716
 
619
- const memoryIds = results.map((row) => row.id)
620
- const relationCounts = await this.fetchRelationCountsBatch(memoryIds)
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
- const processed = processGraphAwareRows(
623
- results,
624
- relationCounts,
625
- options.limit,
626
- (row) => row.linearScore,
627
- WEAK_GRAPH_BOOSTS,
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
- 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
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
- async get(id: string): Promise<MemoryRecord | null> {
762
+ private getEffect(id: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
650
763
  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) }),
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
- const row = results.at(0)
656
- if (!row) return null
657
- return mapRowToMemoryRecord(row)
775
+ get(id: string): Effect.Effect<MemoryRecord | null, MemoryStoreError, never> {
776
+ return this.getEffect(id)
658
777
  }
659
778
 
660
- async update(
779
+ private updateEffect(
661
780
  id: string,
662
781
  newContent: string,
663
782
  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)
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
- await this.recordHistory(id, existing.content, newContent, 'UPDATE')
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
- async delete(id: string): Promise<void> {
696
- const existing = await this.get(id)
697
- if (!existing) return
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
- await databaseService.deleteById(MEMORY_TABLE, id)
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
- await this.recordHistory(id, existing.content, null, 'DELETE')
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
- 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
- }
838
+ delete(id: string): Effect.Effect<void, MemoryStoreError, never> {
839
+ return this.deleteEffect(id)
840
+ }
716
841
 
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
- }
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
- 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
- }
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
- 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
- }
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
- const sql = `
738
- SELECT * FROM ${MEMORY_TABLE}
739
- WHERE ${whereClauses.join('\n AND ')}
740
- ORDER BY createdAt ${sortDirection}
741
- ${limitClause}
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
- const results = await databaseService.query<SurrealMemoryRow>(new BoundQuery(sql, bindVars))
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
- return results.map((row) => mapRowToMemoryRecord(row))
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
- 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)
891
+ list(options: MemoryListOptions): Effect.Effect<MemoryRecord[], MemoryStoreError, never> {
892
+ return this.listEffect(options)
756
893
  }
757
894
 
758
- async addRelation(fromId: string, toId: string, relationType: RelationType, confidence: number = 1.0): Promise<void> {
759
- confidence = clampImportance(confidence)
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
- await databaseService.relate(fromRef, MEMORY_RELATION_TABLE, toRef, { relationType, confidence })
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
- 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
- }
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 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 },
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
- 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`
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
- async getRelatedMemories(
969
+ private getRelatedMemoriesEffect(
793
970
  memoryId: string,
794
971
  relationType?: RelationType,
795
- ): Promise<{ relatesTo: MemoryRecord[]; relatedBy: MemoryRecord[] }> {
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
- const result = await databaseService.query<{ relatesTo: SurrealMemoryRow[]; relatedBy: SurrealMemoryRow[] }>(
806
- new BoundQuery(sql, { memoryId: ensureRecordId(memoryId, TABLES.MEMORY), relationType }),
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
- 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
- }
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
- async findConflicts(scopeId: string): Promise<Array<{ memory: MemoryRecord; contradictedBy: MemoryRecord[] }>> {
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
- const results = await databaseService.query<SurrealMemoryRow & { contradictedBy: SurrealMemoryRow[] }>(
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
- async graphWalk(
1027
+ private graphWalkEffect(
837
1028
  startId: string,
838
1029
  depth = 2,
839
- ): Promise<{
840
- memories: MemoryRecord[]
841
- edges: Array<{ from: string; to: string; relationType: RelationType; confidence: number }>
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
- let frontier = [startId]
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
- 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)
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
- frontier = nextFrontier
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 async recordHistory(
1135
+ private recordHistoryEffect(
906
1136
  memoryId: string,
907
1137
  prevValue: string | null,
908
1138
  newValue: string | null,
909
1139
  event: MemoryEvent,
910
- ): Promise<void> {
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
- await databaseService.insert<Record<string, unknown>>(MEMORY_HISTORY_TABLE, historyRow)
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
- async enrichWithNeighbors(results: MemorySearchResult[], topN: number = 5): Promise<MemorySearchResult[]> {
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
- 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
- }
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
- 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
- })
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
- 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 },
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
- 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
- }))
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
- let defaultMemoryStore: SurrealMemoryStore | null = null
991
-
992
- export function getDefaultMemoryStore(): SurrealMemoryStore {
993
- if (!defaultMemoryStore) {
994
- defaultMemoryStore = new SurrealMemoryStore(getDefaultEmbeddings())
995
- }
996
- return defaultMemoryStore
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
  }