@lota-sdk/core 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  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,14 @@
1
- type OrganizationOnboardStatus = string
2
1
  import type { Job } from 'bullmq'
2
+ import { Effect } from 'effect'
3
+ import type { Context } from 'effect'
4
+ import type IORedis from 'ioredis'
3
5
 
4
- import { databaseService } from '../db/service'
5
- import { memoryService } from '../services/memory.service'
6
+ import type { MaybeAwaitableService } from '../effect/awaitable-effect'
7
+ import { ConfigurationError } from '../effect/errors'
8
+ import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
9
+ import { MemoryServiceTag } from '../services/memory/memory.service'
6
10
  import { createQueueFactory } from './queue-factory'
11
+ import { runStandaloneQueueWorker } from './standalone-worker'
7
12
 
8
13
  interface PostChatMemoryMessage {
9
14
  role: 'user' | 'agent'
@@ -17,7 +22,7 @@ interface PostChatMemoryExtractionJob {
17
22
  sourceId: string
18
23
  source?: string
19
24
  sourceMetadata?: Record<string, unknown>
20
- onboardStatus?: OrganizationOnboardStatus
25
+ onboardStatus?: string
21
26
  userMessage: string
22
27
  historyMessages: PostChatMemoryMessage[]
23
28
  agentMessages: Array<{ content: string; agentName?: string }>
@@ -25,8 +30,23 @@ interface PostChatMemoryExtractionJob {
25
30
  attachmentContext?: string
26
31
  }
27
32
 
28
- async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>): Promise<void> {
29
- await databaseService.connect()
33
+ interface PostChatMemoryQueueDeps {
34
+ databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
35
+ memoryService: MaybeAwaitableService<Context.Service.Shape<typeof MemoryServiceTag>>
36
+ }
37
+
38
+ let _deps: PostChatMemoryQueueDeps | null = null
39
+ function getDeps(): PostChatMemoryQueueDeps {
40
+ if (!_deps)
41
+ throw new ConfigurationError({
42
+ message: 'Post-chat memory queue is not configured. Initialize the runtime before starting the worker.',
43
+ key: 'queue-deps',
44
+ })
45
+ return _deps
46
+ }
47
+
48
+ function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>): Promise<void> {
49
+ const { memoryService } = getDeps()
30
50
 
31
51
  const data = job.data
32
52
  const userMessage = data.userMessage.trim()
@@ -37,7 +57,9 @@ async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>):
37
57
  }))
38
58
  .filter((item) => item.content.length > 0)
39
59
 
40
- if (!userMessage || agentMessages.length === 0) return
60
+ if (!userMessage || agentMessages.length === 0) {
61
+ return Promise.resolve()
62
+ }
41
63
 
42
64
  const joinedOutput = agentMessages
43
65
  .map((item) => (item.agentName ? `[${item.agentName}] ${item.content}` : item.content))
@@ -51,20 +73,24 @@ async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>):
51
73
  ),
52
74
  ]
53
75
 
54
- await memoryService.addConversationMemories({
55
- orgId: data.orgId,
56
- input: userMessage,
57
- output: joinedOutput,
58
- sourceId: data.sourceId,
59
- source: data.source,
60
- sourceMetadata: data.sourceMetadata,
61
- onboardStatus: data.onboardStatus,
62
- ...(uniqueAgentNames.length > 0 ? { agentName: uniqueAgentNames[0] } : {}),
63
- historyMessages: data.historyMessages,
64
- memoryBlock: data.memoryBlock,
65
- attachmentContext: data.attachmentContext,
66
- agentNames: uniqueAgentNames,
67
- })
76
+ return Effect.runPromise(
77
+ Effect.asVoid(
78
+ memoryService.addConversationMemories({
79
+ orgId: data.orgId,
80
+ input: userMessage,
81
+ output: joinedOutput,
82
+ sourceId: data.sourceId,
83
+ source: data.source,
84
+ sourceMetadata: data.sourceMetadata,
85
+ onboardStatus: data.onboardStatus,
86
+ ...(uniqueAgentNames.length > 0 ? { agentName: uniqueAgentNames[0] } : {}),
87
+ historyMessages: data.historyMessages,
88
+ memoryBlock: data.memoryBlock,
89
+ attachmentContext: data.attachmentContext,
90
+ agentNames: uniqueAgentNames,
91
+ }),
92
+ ),
93
+ )
68
94
  }
69
95
 
70
96
  const postChatMemory = createQueueFactory<PostChatMemoryExtractionJob>({
@@ -76,14 +102,30 @@ const postChatMemory = createQueueFactory<PostChatMemoryExtractionJob>({
76
102
  maxStalledCount: 10,
77
103
  stalledInterval: 120_000,
78
104
  defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
105
+ prepare: () => getDeps().databaseService.connect(),
79
106
  processor: processPostChatMemoryJob,
80
107
  })
81
108
 
82
109
  export function enqueuePostChatMemory(job: PostChatMemoryExtractionJob, options?: { dedupeKey?: string }) {
83
110
  return postChatMemory.enqueue(job, options?.dedupeKey ? { jobId: options.dedupeKey } : undefined)
84
111
  }
85
- export const startPostChatMemoryWorker = postChatMemory.startWorker
86
112
 
87
- if (import.meta.main) {
88
- startPostChatMemoryWorker()
113
+ export function startPostChatMemoryWorker(options: {
114
+ registerSignals?: boolean
115
+ connectionProvider: () => IORedis
116
+ deps: PostChatMemoryQueueDeps
117
+ }) {
118
+ _deps = options.deps
119
+ return postChatMemory.startWorker({
120
+ registerSignals: options.registerSignals,
121
+ connectionProvider: options.connectionProvider,
122
+ })
89
123
  }
124
+
125
+ runStandaloneQueueWorker((runtime) => {
126
+ const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
127
+ startPostChatMemoryWorker({
128
+ connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
129
+ deps: { databaseService: resolve(DatabaseServiceTag), memoryService: resolve(MemoryServiceTag) },
130
+ })
131
+ })
@@ -1,24 +1,33 @@
1
1
  import { Queue, Worker } from 'bullmq'
2
2
  import type { Job, JobsOptions, QueueOptions, WorkerOptions } from 'bullmq'
3
+ import { Effect } from 'effect'
3
4
  import type IORedis from 'ioredis'
4
5
 
6
+ import type { LotaLogger } from '../config/logger'
5
7
  import { serverLogger } from '../config/logger'
6
- import { getRedisConnectionForBullMQ } from '../redis'
7
- import { queueJobService } from '../services/queue-job.service'
8
+ import { getCurrentRuntime } from '../effect/runtime-ref'
9
+ import { RedisServiceTag } from '../effect/services'
8
10
  import {
9
11
  attachWorkerEvents,
10
12
  createTracedWorkerProcessor,
11
13
  createWorkerShutdown,
12
14
  DEFAULT_JOB_RETENTION,
13
15
  registerShutdownSignals,
16
+ getQueueJobService,
14
17
  } from '../workers/worker-utils'
15
18
  import type { WorkerHandle } from '../workers/worker-utils'
16
19
 
20
+ function getDefaultQueueConnectionProvider(): () => IORedis {
21
+ const redis = getCurrentRuntime().runSync(Effect.service(RedisServiceTag))
22
+ return () => redis.getConnectionForBullMQ()
23
+ }
24
+
17
25
  interface QueueFactoryConfigBase {
18
26
  name: string
19
27
  displayName: string
20
28
  jobName: string
21
29
  concurrency: number
30
+ logger?: LotaLogger
22
31
  lockDuration?: number
23
32
  stalledInterval?: number
24
33
  maxStalledCount?: number
@@ -27,6 +36,7 @@ interface QueueFactoryConfigBase {
27
36
  }
28
37
 
29
38
  interface QueueFactoryConfigInline<TJob> extends QueueFactoryConfigBase {
39
+ prepare?: (job: Job<TJob>) => Promise<void>
30
40
  processor: (job: Job<TJob>) => Promise<unknown>
31
41
  processorPath?: never
32
42
  }
@@ -41,70 +51,137 @@ export type QueueFactoryConfig<TJob> = QueueFactoryConfigInline<TJob> | QueueFac
41
51
  export interface QueueFactory<TJob> {
42
52
  getQueue: () => Queue<TJob, unknown, string, TJob, unknown, string>
43
53
  enqueue: (job: TJob, options?: JobsOptions) => Promise<void>
44
- startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle
54
+ startWorker: (options?: { registerSignals?: boolean; connectionProvider?: () => IORedis }) => WorkerHandle
45
55
  }
46
56
 
47
57
  export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): QueueFactory<TJob> {
48
58
  type QueueShape = Queue<TJob, unknown, string, TJob, unknown, string>
59
+ type QueueMethod = 'add' | 'close' | 'remove' | 'removeDeduplicationKey' | 'removeJobScheduler'
60
+ const queueMethodsThatWaitForClose = new Set<QueueMethod>([
61
+ 'add',
62
+ 'close',
63
+ 'remove',
64
+ 'removeDeduplicationKey',
65
+ 'removeJobScheduler',
66
+ ])
67
+
68
+ let state: {
69
+ queue: QueueShape | null
70
+ rawQueue: QueueShape | null
71
+ connection: IORedis | null
72
+ pendingClose: Promise<void> | null
73
+ } = { queue: null, rawQueue: null, connection: null, pendingClose: null }
74
+
75
+ const resolveConnectionProvider = (): (() => IORedis) =>
76
+ config.connectionProvider ?? getDefaultQueueConnectionProvider()
77
+
78
+ const getConnection = (): IORedis => resolveConnectionProvider()()
79
+
80
+ const waitForPendingClose = (): Promise<void> => {
81
+ const pendingClose = state.pendingClose
82
+ if (!pendingClose) {
83
+ return Promise.resolve()
84
+ }
49
85
 
50
- let _queue: QueueShape | null = null
51
- let _queueConnection: IORedis | null = null
86
+ return pendingClose.finally(() => {
87
+ if (state.pendingClose === pendingClose) {
88
+ state.pendingClose = null
89
+ }
90
+ })
91
+ }
52
92
 
53
- const getConnection = (): IORedis => config.connectionProvider?.() ?? getRedisConnectionForBullMQ()
93
+ const wrapQueue = (queue: QueueShape): QueueShape =>
94
+ new Proxy(queue, {
95
+ get(target, property, receiver) {
96
+ if (typeof property !== 'string') {
97
+ return Reflect.get(target, property, receiver) as unknown
98
+ }
99
+
100
+ const value = (target as unknown as Record<string, unknown>)[property]
101
+ if (typeof value !== 'function' || !queueMethodsThatWaitForClose.has(property as QueueMethod)) {
102
+ return value
103
+ }
104
+
105
+ return (...args: Array<unknown>) =>
106
+ waitForPendingClose().then(() =>
107
+ Reflect.apply(value as (...methodArgs: Array<unknown>) => unknown, target, args),
108
+ )
109
+ },
110
+ })
54
111
 
55
112
  const getQueue = (): QueueShape => {
56
113
  const connection = getConnection()
57
- const shouldRecreateQueue =
58
- _queue === null ||
59
- _queueConnection === null ||
60
- _queueConnection !== connection ||
61
- _queueConnection.status === 'close' ||
62
- _queueConnection.status === 'end'
63
-
64
- if (shouldRecreateQueue) {
65
- if (_queue) {
66
- void _queue.close().catch((error: unknown) => {
67
- serverLogger.warn`Failed to close stale ${config.displayName} queue: ${error}`
68
- })
69
- }
70
-
71
- _queue = new Queue<TJob, unknown, string, TJob, unknown, string>(config.name, {
72
- connection: connection as QueueOptions['connection'],
73
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, ...config.defaultJobOptions },
74
- })
75
- _queueConnection = connection
114
+ const isStale =
115
+ state.rawQueue === null ||
116
+ state.queue === null ||
117
+ state.connection === null ||
118
+ state.connection !== connection ||
119
+ state.connection.status === 'close' ||
120
+ state.connection.status === 'end'
121
+
122
+ if (!isStale && state.queue) {
123
+ return state.queue
76
124
  }
77
- if (_queue === null) {
78
- throw new Error(`Failed to initialize queue: ${config.name}`)
125
+
126
+ if (state.rawQueue) {
127
+ const staleQueue = state.rawQueue
128
+ const previousPendingClose = state.pendingClose ?? Promise.resolve()
129
+ state.pendingClose = previousPendingClose
130
+ .catch(() => undefined)
131
+ .then(() =>
132
+ staleQueue.close().catch((error: unknown) => {
133
+ serverLogger.warn`Failed to close stale ${config.displayName} queue: ${error}`
134
+ }),
135
+ )
136
+ .then(() => undefined)
79
137
  }
80
- return _queue
138
+
139
+ const queue = new Queue<TJob, unknown, string, TJob, unknown, string>(config.name, {
140
+ connection: connection as QueueOptions['connection'],
141
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, ...config.defaultJobOptions },
142
+ })
143
+ const wrappedQueue = wrapQueue(queue)
144
+
145
+ state = { queue: wrappedQueue, rawQueue: queue, connection, pendingClose: state.pendingClose }
146
+ return wrappedQueue
81
147
  }
82
148
 
83
149
  const jobName = config.jobName
84
150
  const toData = (job: TJob) => job
85
151
 
86
- const enqueue = async (job: TJob, options?: JobsOptions): Promise<void> => {
87
- const queuedJob = await getQueue().add(jobName, toData(job), options)
88
- try {
89
- await queueJobService.recordEnqueued({
90
- queueName: config.name,
91
- id: queuedJob.id,
92
- name: queuedJob.name,
93
- data: queuedJob.data,
94
- opts: queuedJob.opts,
95
- attemptsMade: queuedJob.attemptsMade,
96
- timestamp: queuedJob.timestamp,
97
- })
98
- } catch (error) {
99
- serverLogger.error`Failed to persist queued job metadata (queue=${config.name}, job=${queuedJob.id}): ${error}`
100
- }
101
- }
102
-
103
- const startWorker = (options: { registerSignals?: boolean } = {}): WorkerHandle => {
104
- const { registerSignals = import.meta.main } = options
152
+ const enqueue = (job: TJob, options?: JobsOptions): Promise<void> =>
153
+ Effect.runPromise(
154
+ Effect.gen(function* () {
155
+ const queuedJob = yield* Effect.tryPromise(() => getQueue().add(jobName, toData(job), options))
156
+ yield* getQueueJobService()
157
+ .recordEnqueued({
158
+ queueName: config.name,
159
+ id: queuedJob.id,
160
+ name: queuedJob.name,
161
+ data: queuedJob.data,
162
+ opts: queuedJob.opts,
163
+ attemptsMade: queuedJob.attemptsMade,
164
+ timestamp: queuedJob.timestamp,
165
+ })
166
+ .pipe(
167
+ Effect.tapError((error) =>
168
+ Effect.sync(() => {
169
+ serverLogger.error`Failed to persist queued job metadata (queue=${config.name}, job=${queuedJob.id}): ${error}`
170
+ }),
171
+ ),
172
+ Effect.orElseSucceed(() => undefined),
173
+ )
174
+ }),
175
+ )
176
+
177
+ const startWorker = (
178
+ options: { registerSignals?: boolean; connectionProvider?: () => IORedis } = {},
179
+ ): WorkerHandle => {
180
+ const { registerSignals = import.meta.main, connectionProvider } = options
181
+ const logger = config.logger ?? serverLogger
105
182
 
106
183
  const workerOptions: WorkerOptions = {
107
- connection: getConnection() as QueueOptions['connection'],
184
+ connection: (connectionProvider ?? resolveConnectionProvider())() as QueueOptions['connection'],
108
185
  concurrency: config.concurrency,
109
186
  ...(config.lockDuration !== undefined ? { lockDuration: config.lockDuration } : {}),
110
187
  ...(config.stalledInterval !== undefined ? { stalledInterval: config.stalledInterval } : {}),
@@ -115,16 +192,29 @@ export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): Queu
115
192
  ? new Worker(config.name, config.processorPath, workerOptions)
116
193
  : new Worker(
117
194
  config.name,
118
- createTracedWorkerProcessor(config.name, (config as QueueFactoryConfigInline<TJob>).processor),
195
+ createTracedWorkerProcessor(config.name, (job) =>
196
+ Effect.runPromise(
197
+ Effect.gen(function* () {
198
+ const inlineConfig = config as QueueFactoryConfigInline<TJob>
199
+ const typedJob = job as Job<TJob>
200
+ const prepare = inlineConfig.prepare
201
+ if (prepare) {
202
+ yield* Effect.tryPromise(() => prepare(typedJob))
203
+ }
204
+ const processor = inlineConfig.processor
205
+ return yield* Effect.tryPromise(() => processor(typedJob))
206
+ }),
207
+ ),
208
+ ),
119
209
  workerOptions,
120
210
  )
121
211
 
122
- attachWorkerEvents(worker, config.displayName, serverLogger)
212
+ attachWorkerEvents(worker, config.displayName, logger)
123
213
 
124
- const shutdown = createWorkerShutdown(worker, config.displayName, serverLogger)
214
+ const shutdown = createWorkerShutdown(worker, config.displayName, logger)
125
215
 
126
216
  if (registerSignals) {
127
- registerShutdownSignals({ name: config.displayName, shutdown, logger: serverLogger })
217
+ registerShutdownSignals({ name: config.displayName, shutdown, logger })
128
218
  }
129
219
 
130
220
  return { worker, shutdown }
@@ -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,15 @@
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 { ConfigurationError } from '../effect/errors'
8
+ import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
9
+ import { RecentActivityTitleServiceTag } from '../services/recent-activity-title.service'
10
+ import { ThreadTitleServiceTag } from '../services/thread/thread-title.service'
7
11
  import { createQueueFactory } from './queue-factory'
12
+ import { runStandaloneQueueWorker } from './standalone-worker'
8
13
 
9
14
  export const TITLE_GENERATION_QUEUE = 'title-generation'
10
15
 
@@ -25,14 +30,31 @@ interface RecentActivityTitleRefinementJob {
25
30
 
26
31
  type TitleGenerationJob = ThreadTitleGenerationJob | RecentActivityTitleRefinementJob
27
32
 
28
- async function processTitleGenerationJob(job: Job<TitleGenerationJob>): Promise<void> {
29
- await databaseService.connect()
33
+ interface TitleGenerationQueueDeps {
34
+ databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
35
+ threadTitleService: Context.Service.Shape<typeof ThreadTitleServiceTag>
36
+ recentActivityTitleService: Context.Service.Shape<typeof RecentActivityTitleServiceTag>
37
+ }
38
+
39
+ let _deps: TitleGenerationQueueDeps | null = null
40
+ function getDeps(): TitleGenerationQueueDeps {
41
+ if (!_deps)
42
+ throw new ConfigurationError({
43
+ message: 'Title generation queue is not configured. Initialize the runtime before starting the worker.',
44
+ key: 'queue-deps',
45
+ })
46
+ return _deps
47
+ }
48
+
49
+ function processTitleGenerationJob(job: Job<TitleGenerationJob>): Promise<void> {
50
+ const { threadTitleService, recentActivityTitleService } = getDeps()
30
51
  if (job.data.kind === 'thread-title') {
31
- await threadTitleService.generateAndPersistTitle(ensureRecordId(job.data.threadId), job.data.sourceText)
32
- return
52
+ return Effect.runPromise(
53
+ Effect.asVoid(threadTitleService.generateAndPersistTitle(ensureRecordId(job.data.threadId), job.data.sourceText)),
54
+ )
33
55
  }
34
56
 
35
- await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
57
+ return Effect.runPromise(Effect.asVoid(recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)))
36
58
  }
37
59
 
38
60
  const titleGeneration = createQueueFactory<TitleGenerationJob>({
@@ -42,6 +64,7 @@ const titleGeneration = createQueueFactory<TitleGenerationJob>({
42
64
  concurrency: 10,
43
65
  lockDuration: 300_000,
44
66
  defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
67
+ prepare: () => getDeps().databaseService.connect(),
45
68
  processor: processTitleGenerationJob,
46
69
  })
47
70
 
@@ -56,4 +79,26 @@ export function enqueueRecentActivityTitleRefinement(job: Omit<RecentActivityTit
56
79
  )
57
80
  }
58
81
 
59
- export const startTitleGenerationWorker = titleGeneration.startWorker
82
+ export function startTitleGenerationWorker(options: {
83
+ registerSignals?: boolean
84
+ connectionProvider: () => IORedis
85
+ deps: TitleGenerationQueueDeps
86
+ }): ReturnType<typeof titleGeneration.startWorker> {
87
+ _deps = options.deps
88
+ return titleGeneration.startWorker({
89
+ registerSignals: options.registerSignals,
90
+ connectionProvider: options.connectionProvider,
91
+ })
92
+ }
93
+
94
+ runStandaloneQueueWorker((runtime) => {
95
+ const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
96
+ startTitleGenerationWorker({
97
+ connectionProvider: () => resolve(RedisServiceTag).getConnectionForBullMQ(),
98
+ deps: {
99
+ databaseService: resolve(DatabaseServiceTag),
100
+ threadTitleService: resolve(ThreadTitleServiceTag),
101
+ recentActivityTitleService: resolve(RecentActivityTitleServiceTag),
102
+ },
103
+ })
104
+ })