@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,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
+ })