@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
@@ -0,0 +1,39 @@
1
+ import type { ManagedRuntime } from 'effect'
2
+ import { Schema, Effect } from 'effect'
3
+
4
+ import { serverLogger } from '../config/logger'
5
+ import { initializeSandboxedWorkerRuntime } from '../workers/bootstrap'
6
+
7
+ class StandaloneQueueWorkerError extends Schema.TaggedErrorClass<StandaloneQueueWorkerError>()(
8
+ 'StandaloneQueueWorkerError',
9
+ { message: Schema.String, cause: Schema.Defect },
10
+ ) {}
11
+
12
+ function toStandaloneQueueWorkerError(cause: unknown): StandaloneQueueWorkerError {
13
+ return new StandaloneQueueWorkerError({ message: cause instanceof Error ? cause.message : String(cause), cause })
14
+ }
15
+
16
+ // eslint-disable-next-line typescript-eslint/no-explicit-any -- wildcard for host-provided ManagedRuntime
17
+ export function runStandaloneQueueWorker(start: (runtime: ManagedRuntime.ManagedRuntime<any, any>) => void): void {
18
+ if (!import.meta.main) {
19
+ return
20
+ }
21
+
22
+ void Effect.runFork(
23
+ Effect.gen(function* () {
24
+ const runtime = yield* Effect.tryPromise({
25
+ try: () => initializeSandboxedWorkerRuntime(),
26
+ catch: (cause) => toStandaloneQueueWorkerError(cause),
27
+ })
28
+
29
+ yield* Effect.sync(() => start(runtime))
30
+ }).pipe(
31
+ Effect.catchTag('StandaloneQueueWorkerError', (error) =>
32
+ Effect.sync(() => {
33
+ serverLogger.error`Standalone queue worker failed: ${error.message}`
34
+ process.exit(1)
35
+ }),
36
+ ),
37
+ ),
38
+ )
39
+ }
@@ -1,10 +1,14 @@
1
1
  import type { Job } from 'bullmq'
2
+ import { Effect } from 'effect'
3
+ import type { Context } from 'effect'
4
+ import type IORedis from 'ioredis'
2
5
 
3
6
  import { ensureRecordId } from '../db/record-id'
4
- import { databaseService } from '../db/service'
5
- import { recentActivityTitleService } from '../services/recent-activity-title.service'
6
- import { threadTitleService } from '../services/thread-title.service'
7
- import { createQueueFactory } from './queue-factory'
7
+ import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
8
+ import { RecentActivityTitleServiceTag } from '../services/recent-activity-title.service'
9
+ import { ThreadTitleServiceTag } from '../services/thread/thread-title.service'
10
+ import { createQueueFactoryWithDeps } from './queue-factory'
11
+ import { runStandaloneQueueWorker } from './standalone-worker'
8
12
 
9
13
  export const TITLE_GENERATION_QUEUE = 'title-generation'
10
14
 
@@ -25,23 +29,31 @@ interface RecentActivityTitleRefinementJob {
25
29
 
26
30
  type TitleGenerationJob = ThreadTitleGenerationJob | RecentActivityTitleRefinementJob
27
31
 
28
- async function processTitleGenerationJob(job: Job<TitleGenerationJob>): Promise<void> {
29
- await databaseService.connect()
32
+ interface TitleGenerationQueueDeps {
33
+ databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
34
+ threadTitleService: Context.Service.Shape<typeof ThreadTitleServiceTag>
35
+ recentActivityTitleService: Context.Service.Shape<typeof RecentActivityTitleServiceTag>
36
+ }
37
+
38
+ function processTitleGenerationJob(deps: TitleGenerationQueueDeps, job: Job<TitleGenerationJob>): Promise<void> {
39
+ const { threadTitleService, recentActivityTitleService } = deps
30
40
  if (job.data.kind === 'thread-title') {
31
- await threadTitleService.generateAndPersistTitle(ensureRecordId(job.data.threadId), job.data.sourceText)
32
- return
41
+ return Effect.runPromise(
42
+ Effect.asVoid(threadTitleService.generateAndPersistTitle(ensureRecordId(job.data.threadId), job.data.sourceText)),
43
+ )
33
44
  }
34
45
 
35
- await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
46
+ return Effect.runPromise(Effect.asVoid(recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)))
36
47
  }
37
48
 
38
- const titleGeneration = createQueueFactory<TitleGenerationJob>({
49
+ const titleGeneration = createQueueFactoryWithDeps<TitleGenerationJob, TitleGenerationQueueDeps>({
39
50
  name: TITLE_GENERATION_QUEUE,
40
51
  displayName: 'Title generation',
41
52
  jobName: 'title-generation',
42
53
  concurrency: 10,
43
54
  lockDuration: 300_000,
44
55
  defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
56
+ prepare: ({ databaseService }) => databaseService.connect(),
45
57
  processor: processTitleGenerationJob,
46
58
  })
47
59
 
@@ -56,4 +68,26 @@ export function enqueueRecentActivityTitleRefinement(job: Omit<RecentActivityTit
56
68
  )
57
69
  }
58
70
 
59
- export const startTitleGenerationWorker = titleGeneration.startWorker
71
+ export function startTitleGenerationWorker(options: {
72
+ registerSignals?: boolean
73
+ connectionProvider: () => IORedis
74
+ deps: TitleGenerationQueueDeps
75
+ }): ReturnType<typeof titleGeneration.startWorker> {
76
+ return titleGeneration.startWorker({
77
+ deps: options.deps,
78
+ registerSignals: options.registerSignals,
79
+ connectionProvider: options.connectionProvider,
80
+ })
81
+ }
82
+
83
+ runStandaloneQueueWorker((runtime) => {
84
+ const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
85
+ startTitleGenerationWorker({
86
+ connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
87
+ deps: {
88
+ databaseService: resolve(DatabaseServiceTag),
89
+ threadTitleService: resolve(ThreadTitleServiceTag),
90
+ recentActivityTitleService: resolve(RecentActivityTitleServiceTag),
91
+ },
92
+ })
93
+ })
@@ -1,6 +1,9 @@
1
+ import { Duration, Effect, Exit, Scope } from 'effect'
1
2
  import IORedis from 'ioredis'
2
3
  import type { RedisOptions } from 'ioredis'
3
4
 
5
+ import { RedisError } from '../effect/errors'
6
+ import { effectTryServicePromise } from '../effect/helpers'
4
7
  import { getErrorMessage } from '../utils/errors'
5
8
 
6
9
  export interface RedisConnectionLogger {
@@ -10,7 +13,7 @@ export interface RedisConnectionLogger {
10
13
  error?: (message: string) => void
11
14
  }
12
15
 
13
- interface CreateRedisConnectionManagerOptions {
16
+ export interface CreateRedisConnectionManagerOptions {
14
17
  url: string
15
18
  redisOptions?: RedisOptions
16
19
  healthCheckIntervalMs?: number
@@ -52,134 +55,200 @@ function log(logger: RedisConnectionLogger | undefined, level: keyof RedisConnec
52
55
  }
53
56
  }
54
57
 
55
- class RedisConnectionManagerImpl implements RedisConnectionManager {
56
- private redis: IORedis | null = null
57
- private isHealthy = false
58
- private healthCheckInterval: ReturnType<typeof setInterval> | null = null
59
- private isInitialized = false
60
- private isClosing = false
61
- private isHealthCheckRunning = false
62
-
63
- constructor(private readonly options: CreateRedisConnectionManagerOptions) {
64
- this.initializeConnection()
65
- }
58
+ interface ManagerState {
59
+ isHealthy: boolean
60
+ isClosing: boolean
61
+ }
66
62
 
67
- private initializeConnection(): void {
68
- if (this.isInitialized) {
69
- return
70
- }
63
+ /**
64
+ * Build the IORedis client + listeners as an `Effect.acquireRelease`. Each
65
+ * handler is held as a const ref so the release step can pass the same
66
+ * function to `client.off(...)`, then `quit()` (falling back to
67
+ * `disconnect()`) the client. Prevents tracked-handler leaks on disposal.
68
+ */
69
+ function acquireRedisClient(
70
+ options: CreateRedisConnectionManagerOptions,
71
+ state: ManagerState,
72
+ ): Effect.Effect<IORedis, RedisError, Scope.Scope> {
73
+ return Effect.acquireRelease(
74
+ Effect.try({
75
+ try: () => {
76
+ const redisOptions = options.redisOptions ?? DEFAULT_REDIS_OPTIONS
77
+ const client = new IORedis(options.url, redisOptions)
78
+
79
+ const onConnect = () => {
80
+ log(options.logger, 'info', 'Redis connected')
81
+ state.isHealthy = true
82
+ }
83
+ const onReady = () => {
84
+ log(options.logger, 'info', 'Redis ready')
85
+ state.isHealthy = true
86
+ }
87
+ const onError = (error: Error) => {
88
+ log(options.logger, 'error', `Redis error: ${error.message}`)
89
+ state.isHealthy = false
90
+ }
91
+ const onClose = () => {
92
+ log(
93
+ options.logger,
94
+ state.isClosing ? 'info' : 'warn',
95
+ state.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
96
+ )
97
+ state.isHealthy = false
98
+ }
99
+ const onReconnecting = () => {
100
+ log(options.logger, 'info', 'Redis reconnecting...')
101
+ }
102
+ const onEnd = () => {
103
+ log(
104
+ options.logger,
105
+ state.isClosing ? 'info' : 'warn',
106
+ state.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
107
+ )
108
+ state.isHealthy = false
109
+ }
71
110
 
72
- this.isInitialized = true
111
+ client.on('connect', onConnect)
112
+ client.on('ready', onReady)
113
+ client.on('error', onError)
114
+ client.on('close', onClose)
115
+ client.on('reconnecting', onReconnecting)
116
+ client.on('end', onEnd)
117
+
118
+ // Carry the handler refs alongside the client so the release closure
119
+ // can detach exactly what was attached.
120
+ ;(client as ClientWithHandlers).__lotaHandlers = {
121
+ connect: onConnect,
122
+ ready: onReady,
123
+ error: onError,
124
+ close: onClose,
125
+ reconnecting: onReconnecting,
126
+ end: onEnd,
127
+ }
73
128
 
74
- try {
75
- const redisOptions = this.options.redisOptions ?? DEFAULT_REDIS_OPTIONS
76
- this.redis = new IORedis(this.options.url, redisOptions)
77
- this.setupEventHandlers()
78
- this.startHealthChecks()
79
- } catch (error) {
80
- log(this.options.logger, 'error', `Failed to initialize Redis connection: ${getErrorMessage(error)}`)
81
- }
82
- }
129
+ return client
130
+ },
131
+ catch: (cause) =>
132
+ new RedisError({ message: `Failed to initialize Redis connection: ${getErrorMessage(cause)}`, cause }),
133
+ }),
134
+ (client) =>
135
+ Effect.gen(function* () {
136
+ state.isClosing = true
137
+
138
+ const handlers = (client as ClientWithHandlers).__lotaHandlers
139
+ if (handlers) {
140
+ client.off('connect', handlers.connect)
141
+ client.off('ready', handlers.ready)
142
+ client.off('error', handlers.error)
143
+ client.off('close', handlers.close)
144
+ client.off('reconnecting', handlers.reconnecting)
145
+ client.off('end', handlers.end)
146
+ }
83
147
 
84
- private setupEventHandlers(): void {
85
- if (!this.redis) return
86
-
87
- this.redis.on('connect', () => {
88
- log(this.options.logger, 'info', 'Redis connected')
89
- this.isHealthy = true
90
- })
91
-
92
- this.redis.on('ready', () => {
93
- log(this.options.logger, 'info', 'Redis ready')
94
- this.isHealthy = true
95
- })
96
-
97
- this.redis.on('error', (error: Error) => {
98
- log(this.options.logger, 'error', `Redis error: ${error.message}`)
99
- this.isHealthy = false
100
- })
101
-
102
- this.redis.on('close', () => {
103
- log(
104
- this.options.logger,
105
- this.isClosing ? 'info' : 'warn',
106
- this.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
107
- )
108
- this.isHealthy = false
109
- })
110
-
111
- this.redis.on('reconnecting', () => {
112
- log(this.options.logger, 'info', 'Redis reconnecting...')
113
- })
114
-
115
- this.redis.on('end', () => {
116
- log(
117
- this.options.logger,
118
- this.isClosing ? 'info' : 'warn',
119
- this.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
120
- )
121
- this.isHealthy = false
122
- })
123
- }
148
+ if (client.status !== 'end') {
149
+ const quitExit = yield* Effect.exit(
150
+ effectTryServicePromise(() => client.quit(), 'Failed to close Redis connection manager'),
151
+ )
152
+ if (Exit.isFailure(quitExit)) {
153
+ log(options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
154
+ client.disconnect()
155
+ }
156
+ }
157
+ }),
158
+ )
159
+ }
124
160
 
125
- private startHealthChecks(): void {
126
- if (this.healthCheckInterval) {
127
- return
128
- }
161
+ interface RedisHandlers {
162
+ readonly connect: () => void
163
+ readonly ready: () => void
164
+ readonly error: (error: Error) => void
165
+ readonly close: () => void
166
+ readonly reconnecting: () => void
167
+ readonly end: () => void
168
+ }
129
169
 
130
- const intervalMs = this.options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
131
- this.healthCheckInterval = setInterval(async () => {
132
- if (this.isHealthCheckRunning) return
133
- this.isHealthCheckRunning = true
170
+ type ClientWithHandlers = IORedis & { __lotaHandlers?: RedisHandlers }
171
+
172
+ /**
173
+ * Health-check loop wired via `Effect.forkScoped` so interruption is automatic
174
+ * when the owning scope closes. No more imperative fiber tracking.
175
+ */
176
+ function startHealthCheckFiber(
177
+ client: IORedis,
178
+ options: CreateRedisConnectionManagerOptions,
179
+ state: ManagerState,
180
+ ): Effect.Effect<void, never, Scope.Scope> {
181
+ const intervalMs = options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
182
+ const logger = options.logger
183
+ let isHealthCheckRunning = false
184
+
185
+ const loop = Effect.gen(function* () {
186
+ for (;;) {
187
+ yield* Effect.sleep(Duration.millis(intervalMs))
188
+ if (isHealthCheckRunning) continue
189
+
190
+ isHealthCheckRunning = true
134
191
  try {
135
- if (this.redis && this.redis.status === 'ready') {
136
- await this.redis.ping()
137
- this.isHealthy = true
192
+ if (client.status === 'ready') {
193
+ const pingExit = yield* Effect.exit(effectTryServicePromise(() => client.ping(), 'Redis health check failed'))
194
+ if (Exit.isFailure(pingExit)) {
195
+ log(logger, 'warn', `Redis health check failed: ${getErrorMessage(pingExit.cause)}`)
196
+ state.isHealthy = false
197
+ } else {
198
+ state.isHealthy = true
199
+ }
138
200
  }
139
- } catch (error) {
140
- log(this.options.logger, 'warn', `Redis health check failed: ${getErrorMessage(error)}`)
141
- this.isHealthy = false
142
201
  } finally {
143
- this.isHealthCheckRunning = false
202
+ isHealthCheckRunning = false
144
203
  }
145
- }, intervalMs)
146
- this.healthCheckInterval.unref()
147
- }
148
-
149
- getConnection(): IORedis {
150
- if (!this.redis) {
151
- throw new Error('Redis connection not initialized')
152
204
  }
153
- return this.redis
154
- }
155
-
156
- getConnectionForBullMQ(): IORedis {
157
- return this.getConnection()
158
- }
205
+ })
159
206
 
160
- isConnectionHealthy(): boolean {
161
- return this.isHealthy && this.redis?.status === 'ready'
162
- }
207
+ return Effect.asVoid(Effect.forkScoped(loop))
208
+ }
163
209
 
164
- async closeConnection(): Promise<void> {
165
- if (this.healthCheckInterval) {
166
- clearInterval(this.healthCheckInterval)
167
- this.healthCheckInterval = null
210
+ /**
211
+ * Scoped builder for the Redis connection manager. The IORedis client and
212
+ * health-check fiber are both bound to the surrounding scope, so closing the
213
+ * scope tears down listeners + quits the client + interrupts the fiber.
214
+ */
215
+ export function makeRedisConnectionManager(
216
+ options: CreateRedisConnectionManagerOptions,
217
+ ): Effect.Effect<RedisConnectionManager, RedisError, Scope.Scope> {
218
+ return Effect.gen(function* () {
219
+ const state: ManagerState = { isHealthy: false, isClosing: false }
220
+ const client = yield* acquireRedisClient(options, state)
221
+ yield* startHealthCheckFiber(client, options, state)
222
+
223
+ const manager: RedisConnectionManager = {
224
+ getConnection: () => client,
225
+ getConnectionForBullMQ: () => client,
226
+ isConnectionHealthy: () => state.isHealthy && client.status === 'ready',
227
+ closeConnection: () => Promise.resolve(),
168
228
  }
169
229
 
170
- this.isClosing = true
171
-
172
- try {
173
- if (this.redis && this.redis.status !== 'end') {
174
- await this.redis.quit()
175
- }
176
- } catch (error) {
177
- log(this.options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(error)}`)
178
- this.redis?.disconnect()
179
- }
180
- }
230
+ return manager
231
+ })
181
232
  }
182
233
 
234
+ /**
235
+ * Standalone Promise-friendly factory for hosts that build the manager outside
236
+ * a Layer. Owns its own `Scope`; `closeConnection()` closes that scope, which
237
+ * runs the same finalizer chain (listener removal, quit/disconnect, fiber
238
+ * interrupt) that `Layer.effect` runs at runtime shutdown.
239
+ */
183
240
  export function createRedisConnectionManager(options: CreateRedisConnectionManagerOptions): RedisConnectionManager {
184
- return new RedisConnectionManagerImpl(options)
241
+ const scope = Scope.makeUnsafe()
242
+ const built = Effect.runSync(Scope.provide(makeRedisConnectionManager(options), scope))
243
+ let closePromise: Promise<void> | null = null
244
+
245
+ return {
246
+ ...built,
247
+ closeConnection: () => {
248
+ if (!closePromise) {
249
+ closePromise = Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
250
+ }
251
+ return closePromise
252
+ },
253
+ }
185
254
  }
@@ -1,15 +1,13 @@
1
1
  import { createRedisConnectionManager } from './connection'
2
2
  import type { RedisConnectionManager } from './connection'
3
3
  export { DEFAULT_REDIS_OPTIONS, type RedisConnectionLogger } from './connection'
4
+ export { withOrgMemoryLock, withOrgMemoryLockEffect } from './org-memory-lock'
5
+ export { withLeaseLock } from './redis-lease-lock'
4
6
  export {
5
- getRedisConnection,
6
- getRedisConnectionForBullMQ,
7
- setRedisConnectionManager,
8
- type RedisConnectionAccessor,
9
- } from './connection-accessor'
10
- export { withOrgMemoryLock } from './org-memory-lock'
11
- export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
12
- export { closeSharedSubscriber, createThreadResumableContext } from './stream-context'
7
+ createThreadResumableContext,
8
+ SharedThreadStreamSubscriberLive,
9
+ SharedThreadStreamSubscriberTag,
10
+ } from './stream-context'
13
11
 
14
12
  export { createRedisConnectionManager }
15
13
  export type { RedisConnectionManager }
@@ -1,6 +1,12 @@
1
+ import { Schema, Effect } from 'effect'
2
+ import type IORedis from 'ioredis'
3
+
1
4
  import { serverLogger } from '../config/logger'
2
- import { getRedisConnection } from './connection-accessor'
3
- import { withRedisLeaseLock } from './redis-lease-lock'
5
+ import { LockAcquisitionError } from '../effect/errors'
6
+ import type { LockLostError, RedisError } from '../effect/errors'
7
+ import { withLeaseLock } from './redis-lease-lock'
8
+ import type { RedisLeaseLockOptions } from './redis-lease-lock'
9
+ import { getRuntimeRedisConnection } from './runtime-connection'
4
10
 
5
11
  const ORG_MEMORY_LOCK_PREFIX = 'lock:org-memory:org:'
6
12
  const ORG_MEMORY_LOCK_TTL_MS = 120_000
@@ -9,35 +15,62 @@ const ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS = 30_000
9
15
  const ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS = 30_000
10
16
  const ORG_MEMORY_LOCK_MAX_WAIT_MS = 15 * 60 * 1000
11
17
 
12
- export async function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
18
+ class OrgMemoryLockCallbackError extends Schema.TaggedErrorClass<OrgMemoryLockCallbackError>()(
19
+ 'OrgMemoryLockCallbackError',
20
+ { message: Schema.String, cause: Schema.Defect },
21
+ ) {}
22
+
23
+ function createOrgMemoryLockOptions(redis: IORedis, orgId: string): RedisLeaseLockOptions & { redis: IORedis } {
24
+ return {
25
+ redis,
26
+ lockKey: `${ORG_MEMORY_LOCK_PREFIX}${orgId}`,
27
+ lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
28
+ retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
29
+ refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
30
+ waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
31
+ maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
32
+ label: 'org memory lock',
33
+ logger: {
34
+ debug: (message) => {
35
+ serverLogger.debug`${message}`
36
+ },
37
+ info: (message) => {
38
+ serverLogger.info`${message}`
39
+ },
40
+ warn: (message) => {
41
+ serverLogger.warn`${message}`
42
+ },
43
+ },
44
+ }
45
+ }
46
+
47
+ export function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
48
+ return Effect.runPromise(
49
+ withOrgMemoryLockEffect(orgId, (signal) =>
50
+ Effect.tryPromise({
51
+ try: () => fn(signal),
52
+ catch: (cause) => new OrgMemoryLockCallbackError({ message: 'Org memory lock callback failed.', cause }),
53
+ }),
54
+ ),
55
+ )
56
+ }
57
+
58
+ export function withOrgMemoryLockEffect<A, E>(
59
+ orgId: string,
60
+ fn: (signal: AbortSignal) => Effect.Effect<A, E>,
61
+ ): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError> {
13
62
  const normalizedOrgId = orgId.trim()
14
63
 
15
64
  if (!normalizedOrgId) {
16
- throw new Error('Organization id is required for memory lock')
65
+ return Effect.fail(
66
+ new LockAcquisitionError({
67
+ lockKey: `${ORG_MEMORY_LOCK_PREFIX}<missing-org-id>`,
68
+ maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
69
+ }),
70
+ )
17
71
  }
18
72
 
19
- return withRedisLeaseLock(
20
- {
21
- redis: getRedisConnection(),
22
- lockKey: `${ORG_MEMORY_LOCK_PREFIX}${normalizedOrgId}`,
23
- lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
24
- retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
25
- refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
26
- waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
27
- maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
28
- label: 'org memory lock',
29
- logger: {
30
- debug: (message) => {
31
- serverLogger.debug`${message}`
32
- },
33
- info: (message) => {
34
- serverLogger.info`${message}`
35
- },
36
- warn: (message) => {
37
- serverLogger.warn`${message}`
38
- },
39
- },
40
- },
41
- fn,
73
+ return Effect.sync(() => getRuntimeRedisConnection()).pipe(
74
+ Effect.flatMap((redis) => withLeaseLock(createOrgMemoryLockOptions(redis, normalizedOrgId), fn)),
42
75
  )
43
76
  }