@lota-sdk/core 0.1.15 → 0.1.17

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 (159) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +12 -8
  9. package/src/ai/definitions.ts +81 -3
  10. package/src/ai/embedding-cache.ts +2 -4
  11. package/src/ai/index.ts +0 -2
  12. package/src/bifrost/bifrost.ts +2 -7
  13. package/src/bifrost/cache-headers.ts +8 -0
  14. package/src/bifrost/index.ts +1 -0
  15. package/src/config/agent-defaults.ts +31 -21
  16. package/src/config/agent-types.ts +11 -0
  17. package/src/config/constants.ts +2 -14
  18. package/src/config/debug-logger.ts +5 -1
  19. package/src/config/index.ts +3 -0
  20. package/src/config/model-constants.ts +16 -34
  21. package/src/config/search.ts +1 -15
  22. package/src/create-runtime.ts +269 -178
  23. package/src/db/cursor-pagination.ts +3 -6
  24. package/src/db/index.ts +2 -0
  25. package/src/db/memory-store.helpers.ts +1 -3
  26. package/src/db/memory-store.rows.ts +7 -7
  27. package/src/db/memory-store.ts +14 -18
  28. package/src/db/memory.ts +13 -13
  29. package/src/db/schema-fingerprint.ts +1 -3
  30. package/src/db/service.ts +153 -79
  31. package/src/db/startup.ts +6 -10
  32. package/src/db/surreal-mutation.ts +43 -0
  33. package/src/db/tables.ts +7 -0
  34. package/src/db/workstream-message-row.ts +15 -0
  35. package/src/embeddings/provider.ts +1 -1
  36. package/src/queues/context-compaction.queue.ts +15 -46
  37. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  38. package/src/queues/document-processor.queue.ts +2 -4
  39. package/src/queues/index.ts +3 -0
  40. package/src/queues/memory-consolidation.queue.ts +16 -51
  41. package/src/queues/plan-scheduler.queue.ts +97 -0
  42. package/src/queues/post-chat-memory.queue.ts +20 -55
  43. package/src/queues/queue-factory.ts +100 -0
  44. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  45. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  46. package/src/queues/skill-extraction.queue.ts +15 -47
  47. package/src/queues/workstream-title-generation.queue.ts +15 -47
  48. package/src/redis/connection.ts +6 -0
  49. package/src/redis/index.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +1 -2
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +109 -35
  53. package/src/runtime/approval-continuation.ts +12 -6
  54. package/src/runtime/context-compaction-runtime.ts +1 -1
  55. package/src/runtime/context-compaction.ts +24 -64
  56. package/src/runtime/execution-plan.ts +22 -18
  57. package/src/runtime/graph-designer.ts +15 -0
  58. package/src/runtime/helper-model.ts +9 -197
  59. package/src/runtime/index.ts +3 -1
  60. package/src/runtime/llm-content.ts +1 -1
  61. package/src/runtime/memory-block.ts +9 -11
  62. package/src/runtime/memory-pipeline.ts +6 -9
  63. package/src/runtime/plugin-resolution.ts +35 -0
  64. package/src/runtime/plugin-types.ts +72 -0
  65. package/src/runtime/retrieval-adapters.ts +1 -1
  66. package/src/runtime/runtime-config.ts +111 -14
  67. package/src/runtime/runtime-extensions.ts +2 -3
  68. package/src/runtime/runtime-worker-registry.ts +6 -0
  69. package/src/runtime/social-chat.ts +752 -0
  70. package/src/runtime/team-consultation-orchestrator.ts +45 -32
  71. package/src/runtime/team-consultation-prompts.ts +11 -2
  72. package/src/runtime/title-helpers.ts +2 -4
  73. package/src/runtime/workstream-chat-helpers.ts +1 -1
  74. package/src/services/adaptive-playbook.service.ts +152 -0
  75. package/src/services/agent-executor.service.ts +292 -0
  76. package/src/services/artifact-provenance.service.ts +172 -0
  77. package/src/services/attachment.service.ts +6 -11
  78. package/src/services/context-compaction.service.ts +72 -55
  79. package/src/services/context-enrichment.service.ts +33 -0
  80. package/src/services/coordination-registry.service.ts +117 -0
  81. package/src/services/document-chunk.service.ts +2 -4
  82. package/src/services/domain-agent-executor.service.ts +71 -0
  83. package/src/services/execution-plan.service.ts +269 -50
  84. package/src/services/feedback-loop.service.ts +96 -0
  85. package/src/services/global-orchestrator.service.ts +148 -0
  86. package/src/services/index.ts +27 -0
  87. package/src/services/institutional-memory.service.ts +145 -0
  88. package/src/services/learned-skill.service.ts +24 -5
  89. package/src/services/memory-assessment.service.ts +3 -2
  90. package/src/services/memory-utils.ts +3 -8
  91. package/src/services/memory.service.ts +49 -61
  92. package/src/services/monitoring-window.service.ts +86 -0
  93. package/src/services/mutating-approval.service.ts +1 -1
  94. package/src/services/node-workspace.service.ts +155 -0
  95. package/src/services/notification.service.ts +39 -0
  96. package/src/services/organization-member.service.ts +11 -4
  97. package/src/services/organization.service.ts +5 -5
  98. package/src/services/ownership-dispatcher.service.ts +403 -0
  99. package/src/services/plan-approval.service.ts +1 -1
  100. package/src/services/plan-builder.service.ts +1 -0
  101. package/src/services/plan-checkpoint.service.ts +30 -2
  102. package/src/services/plan-compiler.service.ts +5 -0
  103. package/src/services/plan-coordination.service.ts +152 -0
  104. package/src/services/plan-cycle.service.ts +284 -0
  105. package/src/services/plan-deadline.service.ts +287 -0
  106. package/src/services/plan-executor.service.ts +384 -40
  107. package/src/services/plan-run.service.ts +41 -7
  108. package/src/services/plan-scheduler.service.ts +240 -0
  109. package/src/services/plan-template.service.ts +117 -0
  110. package/src/services/plan-validator.service.ts +84 -2
  111. package/src/services/plan-workspace.service.ts +83 -0
  112. package/src/services/playbook-registry.service.ts +67 -0
  113. package/src/services/plugin-executor.service.ts +103 -0
  114. package/src/services/quality-metrics.service.ts +132 -0
  115. package/src/services/recent-activity.service.ts +28 -34
  116. package/src/services/skill-resolver.service.ts +19 -0
  117. package/src/services/social-chat-history.service.ts +197 -0
  118. package/src/services/system-executor.service.ts +105 -0
  119. package/src/services/workstream-message.service.ts +13 -37
  120. package/src/services/workstream-plan-registry.service.ts +22 -0
  121. package/src/services/workstream-title.service.ts +3 -1
  122. package/src/services/workstream-turn-preparation.service.ts +34 -89
  123. package/src/services/workstream.service.ts +33 -55
  124. package/src/services/workstream.types.ts +9 -9
  125. package/src/services/write-intent-validator.service.ts +81 -0
  126. package/src/storage/attachment-parser.ts +1 -1
  127. package/src/storage/attachment-utils.ts +1 -1
  128. package/src/storage/generated-document-storage.service.ts +3 -2
  129. package/src/system-agents/context-compaction.agent.ts +2 -0
  130. package/src/system-agents/delegated-agent-factory.ts +5 -0
  131. package/src/system-agents/memory-reranker.agent.ts +4 -2
  132. package/src/system-agents/memory.agent.ts +2 -0
  133. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
  134. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
  135. package/src/system-agents/skill-extractor.agent.ts +2 -0
  136. package/src/system-agents/skill-manager.agent.ts +2 -0
  137. package/src/system-agents/title-generator.agent.ts +2 -0
  138. package/src/tools/execution-plan.tool.ts +17 -23
  139. package/src/tools/index.ts +0 -1
  140. package/src/tools/research-topic.tool.ts +2 -0
  141. package/src/tools/team-think.tool.ts +5 -6
  142. package/src/utils/async.ts +2 -1
  143. package/src/utils/date-time.ts +4 -32
  144. package/src/utils/env.ts +8 -0
  145. package/src/utils/errors.ts +42 -10
  146. package/src/utils/index.ts +9 -0
  147. package/src/utils/string.ts +114 -1
  148. package/src/workers/index.ts +1 -0
  149. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  150. package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
  151. package/src/workers/skill-extraction.runner.ts +26 -6
  152. package/src/workers/utils/file-section-chunker.ts +2 -1
  153. package/src/workers/utils/repo-structure-extractor.ts +2 -2
  154. package/src/workers/utils/repomix-file-sections.ts +2 -2
  155. package/src/workers/utils/sandbox-error.ts +11 -2
  156. package/src/workers/utils/workstream-message-query.ts +14 -25
  157. package/src/workers/worker-utils.ts +2 -2
  158. package/src/runtime/workstream-routing-policy.ts +0 -267
  159. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -0,0 +1,100 @@
1
+ import { Queue, Worker } from 'bullmq'
2
+ import type { Job, JobsOptions, WorkerOptions } from 'bullmq'
3
+ import type IORedis from 'ioredis'
4
+
5
+ import { serverLogger } from '../config/logger'
6
+ import { getRedisConnectionForBullMQ } from '../redis'
7
+ import {
8
+ attachWorkerEvents,
9
+ createTracedWorkerProcessor,
10
+ createWorkerShutdown,
11
+ DEFAULT_JOB_RETENTION,
12
+ registerShutdownSignals,
13
+ } from '../workers/worker-utils'
14
+ import type { WorkerHandle } from '../workers/worker-utils'
15
+
16
+ interface QueueFactoryConfigBase {
17
+ name: string
18
+ displayName: string
19
+ jobName: string
20
+ concurrency: number
21
+ lockDuration?: number
22
+ stalledInterval?: number
23
+ maxStalledCount?: number
24
+ defaultJobOptions?: JobsOptions
25
+ connectionProvider?: () => IORedis
26
+ }
27
+
28
+ interface QueueFactoryConfigInline<TJob> extends QueueFactoryConfigBase {
29
+ processor: (job: Job<TJob>) => Promise<void>
30
+ processorPath?: never
31
+ }
32
+
33
+ interface QueueFactoryConfigFile extends QueueFactoryConfigBase {
34
+ processor?: never
35
+ processorPath: string
36
+ }
37
+
38
+ export type QueueFactoryConfig<TJob> = QueueFactoryConfigInline<TJob> | QueueFactoryConfigFile
39
+
40
+ export interface QueueFactory<TJob> {
41
+ getQueue: () => Queue<TJob, unknown, string>
42
+ enqueue: (job: TJob, options?: JobsOptions) => Promise<void>
43
+ startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle
44
+ }
45
+
46
+ export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): QueueFactory<TJob> {
47
+ let _queue: Queue<TJob, unknown, string> | null = null
48
+
49
+ const getConnection = () => config.connectionProvider?.() ?? getRedisConnectionForBullMQ()
50
+
51
+ const getQueue = (): Queue<TJob, unknown, string> => {
52
+ if (!_queue) {
53
+ _queue = new Queue<TJob, unknown, string>(config.name, {
54
+ connection: getConnection(),
55
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, ...config.defaultJobOptions },
56
+ })
57
+ }
58
+ return _queue
59
+ }
60
+
61
+ type QueueAdd = Queue<TJob, unknown, string>['add']
62
+ const jobName = config.jobName as Parameters<QueueAdd>[0]
63
+ const toData = (job: TJob) => job as Parameters<QueueAdd>[1]
64
+
65
+ const enqueue = async (job: TJob, options?: JobsOptions): Promise<void> => {
66
+ await getQueue().add(jobName, toData(job), options)
67
+ }
68
+
69
+ const startWorker = (options: { registerSignals?: boolean } = {}): WorkerHandle => {
70
+ const { registerSignals = import.meta.main } = options
71
+
72
+ const workerOptions: WorkerOptions = {
73
+ connection: getConnection(),
74
+ concurrency: config.concurrency,
75
+ ...(config.lockDuration !== undefined ? { lockDuration: config.lockDuration } : {}),
76
+ ...(config.stalledInterval !== undefined ? { stalledInterval: config.stalledInterval } : {}),
77
+ ...(config.maxStalledCount !== undefined ? { maxStalledCount: config.maxStalledCount } : {}),
78
+ }
79
+
80
+ const worker = config.processorPath
81
+ ? new Worker(config.name, config.processorPath, workerOptions)
82
+ : new Worker(
83
+ config.name,
84
+ createTracedWorkerProcessor(config.name, (config as QueueFactoryConfigInline<TJob>).processor),
85
+ workerOptions,
86
+ )
87
+
88
+ attachWorkerEvents(worker, config.displayName, serverLogger)
89
+
90
+ const shutdown = createWorkerShutdown(worker, config.displayName, serverLogger)
91
+
92
+ if (registerSignals) {
93
+ registerShutdownSignals({ name: config.displayName, shutdown, logger: serverLogger })
94
+ }
95
+
96
+ return { worker, shutdown }
97
+ }
98
+
99
+ return { getQueue, enqueue, startWorker }
100
+ }
@@ -1,65 +1,30 @@
1
- import { Queue, Worker } from 'bullmq'
2
1
  import type { Job } from 'bullmq'
3
2
 
4
- import { serverLogger } from '../config/logger'
5
3
  import { databaseService } from '../db/service'
6
- import { getRedisConnectionForBullMQ } from '../redis'
7
4
  import { recentActivityTitleService } from '../services/recent-activity-title.service'
8
- import {
9
- attachWorkerEvents,
10
- createTracedWorkerProcessor,
11
- createWorkerShutdown,
12
- DEFAULT_JOB_RETENTION,
13
- registerShutdownSignals,
14
- } from '../workers/worker-utils'
15
- import type { WorkerHandle } from '../workers/worker-utils'
5
+ import { createQueueFactory } from './queue-factory'
16
6
 
17
7
  interface RecentActivityTitleRefinementJob {
18
8
  activityId: string
19
9
  }
20
10
 
21
- const RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE = 'recent-activity-title-refinement'
22
-
23
- let _recentActivityTitleRefinementQueue: Queue<RecentActivityTitleRefinementJob> | null = null
24
- function getRecentActivityTitleRefinementQueue(): Queue<RecentActivityTitleRefinementJob> {
25
- if (!_recentActivityTitleRefinementQueue) {
26
- _recentActivityTitleRefinementQueue = new Queue<RecentActivityTitleRefinementJob>(
27
- RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE,
28
- {
29
- connection: getRedisConnectionForBullMQ(),
30
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
31
- },
32
- )
33
- }
34
- return _recentActivityTitleRefinementQueue
35
- }
36
-
37
- export async function enqueueRecentActivityTitleRefinement(job: RecentActivityTitleRefinementJob) {
38
- return await getRecentActivityTitleRefinementQueue().add('refine-recent-activity-title', job, {
39
- jobId: `recent-activity-title:${job.activityId}`,
40
- })
41
- }
42
-
43
11
  async function processRecentActivityTitleRefinementJob(job: Job<RecentActivityTitleRefinementJob>): Promise<void> {
44
12
  await databaseService.connect()
45
13
  await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
46
14
  }
47
15
 
48
- export function startRecentActivityTitleRefinementWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const worker = new Worker(
51
- RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE,
52
- createTracedWorkerProcessor(RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE, processRecentActivityTitleRefinementJob),
53
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 300_000 },
54
- )
55
-
56
- attachWorkerEvents(worker, 'Recent activity title refinement', serverLogger)
57
-
58
- const shutdown = createWorkerShutdown(worker, 'Recent activity title refinement', serverLogger)
59
-
60
- if (registerSignals) {
61
- registerShutdownSignals({ name: 'Recent activity title refinement', shutdown, logger: serverLogger })
62
- }
63
-
64
- return { worker, shutdown }
16
+ const recentActivityTitleRefinement = createQueueFactory<RecentActivityTitleRefinementJob>({
17
+ name: 'recent-activity-title-refinement',
18
+ displayName: 'Recent activity title refinement',
19
+ jobName: 'refine-recent-activity-title',
20
+ concurrency: 10,
21
+ lockDuration: 300_000,
22
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
23
+ processor: processRecentActivityTitleRefinementJob,
24
+ })
25
+
26
+ export function enqueueRecentActivityTitleRefinement(job: RecentActivityTitleRefinementJob) {
27
+ return recentActivityTitleRefinement.enqueue(job, { jobId: `recent-activity-title:${job.activityId}` })
65
28
  }
29
+
30
+ export const startRecentActivityTitleRefinementWorker = recentActivityTitleRefinement.startWorker
@@ -1,16 +1,5 @@
1
- import { Queue, Worker } from 'bullmq'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { getRedisConnectionForBullMQ } from '../redis'
5
- import {
6
- attachWorkerEvents,
7
- DEFAULT_JOB_RETENTION,
8
- getWorkerPath,
9
- LONG_JOB_LOCK_DURATION_MS,
10
- createWorkerShutdown,
11
- registerShutdownSignals,
12
- } from '../workers/worker-utils'
13
- import type { WorkerHandle } from '../workers/worker-utils'
1
+ import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
14
3
  import {
15
4
  buildRegularChatMemoryDigestDeduplicationId,
16
5
  buildRegularChatMemoryDigestJobOptions,
@@ -20,50 +9,25 @@ export interface RegularChatMemoryDigestJob {
20
9
  orgId: string
21
10
  }
22
11
 
23
- const REGULAR_CHAT_MEMORY_DIGEST_QUEUE = 'regular-chat-memory-digest'
24
-
25
- let _regularChatMemoryDigestQueue: Queue<RegularChatMemoryDigestJob> | null = null
26
- function getRegularChatMemoryDigestQueue(): Queue<RegularChatMemoryDigestJob> {
27
- if (!_regularChatMemoryDigestQueue) {
28
- _regularChatMemoryDigestQueue = new Queue<RegularChatMemoryDigestJob>(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, {
29
- connection: getRedisConnectionForBullMQ(),
30
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
31
- })
32
- }
33
- return _regularChatMemoryDigestQueue
34
- }
35
-
36
- export async function enqueueRegularChatMemoryDigest(job: RegularChatMemoryDigestJob) {
37
- return await getRegularChatMemoryDigestQueue().add(
38
- 'run-digest',
39
- job,
40
- buildRegularChatMemoryDigestJobOptions(job.orgId),
41
- )
12
+ const regularChatMemoryDigest = createQueueFactory<RegularChatMemoryDigestJob>({
13
+ name: 'regular-chat-memory-digest',
14
+ displayName: 'Regular chat memory digest',
15
+ jobName: 'run-digest',
16
+ concurrency: 1,
17
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
18
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
19
+ processorPath: getWorkerPath('regular-chat-memory-digest.worker.ts'),
20
+ })
21
+
22
+ export function enqueueRegularChatMemoryDigest(job: RegularChatMemoryDigestJob) {
23
+ return regularChatMemoryDigest.enqueue(job, buildRegularChatMemoryDigestJobOptions(job.orgId))
42
24
  }
43
25
 
44
26
  export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
45
- await getRegularChatMemoryDigestQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
27
+ await regularChatMemoryDigest.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
46
28
  }
47
29
 
48
- export function startRegularChatMemoryDigestWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const processorPath = getWorkerPath('regular-chat-memory-digest.worker.ts')
51
- const worker = new Worker(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, processorPath, {
52
- connection: getRedisConnectionForBullMQ(),
53
- concurrency: 1,
54
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
55
- })
56
-
57
- attachWorkerEvents(worker, 'Regular chat memory digest', serverLogger)
58
-
59
- const shutdown = createWorkerShutdown(worker, 'Regular chat memory digest', serverLogger)
60
-
61
- if (registerSignals) {
62
- registerShutdownSignals({ name: 'Regular chat memory digest', shutdown, logger: serverLogger })
63
- }
64
-
65
- return { worker, shutdown }
66
- }
30
+ export const startRegularChatMemoryDigestWorker = regularChatMemoryDigest.startWorker
67
31
 
68
32
  if (import.meta.main) {
69
33
  startRegularChatMemoryDigestWorker()
@@ -1,58 +1,26 @@
1
- import { Queue, Worker } from 'bullmq'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { getRedisConnectionForBullMQ } from '../redis'
5
- import {
6
- attachWorkerEvents,
7
- DEFAULT_JOB_RETENTION,
8
- getWorkerPath,
9
- LONG_JOB_LOCK_DURATION_MS,
10
- createWorkerShutdown,
11
- registerShutdownSignals,
12
- } from '../workers/worker-utils'
13
- import type { WorkerHandle } from '../workers/worker-utils'
1
+ import { getWorkerPath } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
14
3
  import { buildSkillExtractionJobOptions } from './skill-extraction.config'
15
4
 
16
5
  export interface SkillExtractionJob {
17
6
  orgId: string
18
7
  }
19
8
 
20
- const SKILL_EXTRACTION_QUEUE = 'skill-extraction'
21
-
22
- let _skillExtractionQueue: Queue<SkillExtractionJob> | null = null
23
- function getSkillExtractionQueue(): Queue<SkillExtractionJob> {
24
- if (!_skillExtractionQueue) {
25
- _skillExtractionQueue = new Queue<SkillExtractionJob>(SKILL_EXTRACTION_QUEUE, {
26
- connection: getRedisConnectionForBullMQ(),
27
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
28
- })
29
- }
30
- return _skillExtractionQueue
9
+ const skillExtraction = createQueueFactory<SkillExtractionJob>({
10
+ name: 'skill-extraction',
11
+ displayName: 'Skill extraction',
12
+ jobName: 'run-extraction',
13
+ concurrency: 10,
14
+ lockDuration: 600_000,
15
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
16
+ processorPath: getWorkerPath('skill-extraction.worker.ts'),
17
+ })
18
+
19
+ export function enqueueSkillExtraction(job: SkillExtractionJob) {
20
+ return skillExtraction.enqueue(job, buildSkillExtractionJobOptions(job.orgId))
31
21
  }
32
22
 
33
- export async function enqueueSkillExtraction(job: SkillExtractionJob) {
34
- return await getSkillExtractionQueue().add('run-extraction', job, buildSkillExtractionJobOptions(job.orgId))
35
- }
36
-
37
- export function startSkillExtractionWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
38
- const { registerSignals = import.meta.main } = options
39
- const processorPath = getWorkerPath('skill-extraction.worker.ts')
40
- const worker = new Worker(SKILL_EXTRACTION_QUEUE, processorPath, {
41
- connection: getRedisConnectionForBullMQ(),
42
- concurrency: 1,
43
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
44
- })
45
-
46
- attachWorkerEvents(worker, 'Skill extraction', serverLogger)
47
-
48
- const shutdown = createWorkerShutdown(worker, 'Skill extraction', serverLogger)
49
-
50
- if (registerSignals) {
51
- registerShutdownSignals({ name: 'Skill extraction', shutdown, logger: serverLogger })
52
- }
53
-
54
- return { worker, shutdown }
55
- }
23
+ export const startSkillExtractionWorker = skillExtraction.startWorker
56
24
 
57
25
  if (import.meta.main) {
58
26
  startSkillExtractionWorker()
@@ -1,65 +1,33 @@
1
- import { Queue, Worker } from 'bullmq'
2
1
  import type { Job } from 'bullmq'
3
2
 
4
- import { serverLogger } from '../config/logger'
5
3
  import { ensureRecordId } from '../db/record-id'
6
4
  import { databaseService } from '../db/service'
7
- import { getRedisConnectionForBullMQ } from '../redis'
8
5
  import { workstreamTitleService } from '../services/workstream-title.service'
9
- import {
10
- attachWorkerEvents,
11
- createTracedWorkerProcessor,
12
- createWorkerShutdown,
13
- DEFAULT_JOB_RETENTION,
14
- registerShutdownSignals,
15
- } from '../workers/worker-utils'
16
- import type { WorkerHandle } from '../workers/worker-utils'
6
+ import { createQueueFactory } from './queue-factory'
17
7
 
18
8
  interface WorkstreamTitleGenerationJob {
19
9
  workstreamId: string
20
10
  sourceText: string
21
11
  }
22
12
 
23
- const WORKSTREAM_TITLE_GENERATION_QUEUE = 'workstream-title-generation'
24
-
25
- let _workstreamTitleGenerationQueue: Queue<WorkstreamTitleGenerationJob> | null = null
26
- function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob> {
27
- if (!_workstreamTitleGenerationQueue) {
28
- _workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
29
- connection: getRedisConnectionForBullMQ(),
30
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 2_000 } },
31
- })
32
- }
33
- return _workstreamTitleGenerationQueue
34
- }
35
-
36
- export async function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
37
- return await getWorkstreamTitleGenerationQueue().add('generate-workstream-title', job, {
38
- jobId: `workstream-title:${job.workstreamId}`,
39
- })
40
- }
41
-
42
13
  async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
43
14
  await databaseService.connect()
44
15
  const workstreamRef = ensureRecordId(job.data.workstreamId)
45
16
  await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
46
17
  }
47
18
 
48
- export function startWorkstreamTitleGenerationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const worker = new Worker(
51
- WORKSTREAM_TITLE_GENERATION_QUEUE,
52
- createTracedWorkerProcessor(WORKSTREAM_TITLE_GENERATION_QUEUE, processWorkstreamTitleGenerationJob),
53
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 60_000 },
54
- )
55
-
56
- attachWorkerEvents(worker, 'Workstream title generation', serverLogger)
57
-
58
- const shutdown = createWorkerShutdown(worker, 'Workstream title generation', serverLogger)
59
-
60
- if (registerSignals) {
61
- registerShutdownSignals({ name: 'Workstream title generation', shutdown, logger: serverLogger })
62
- }
63
-
64
- return { worker, shutdown }
19
+ const workstreamTitleGeneration = createQueueFactory<WorkstreamTitleGenerationJob>({
20
+ name: 'workstream-title-generation',
21
+ displayName: 'Workstream title generation',
22
+ jobName: 'generate-workstream-title',
23
+ concurrency: 10,
24
+ lockDuration: 60_000,
25
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 2_000 } },
26
+ processor: processWorkstreamTitleGenerationJob,
27
+ })
28
+
29
+ export function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
30
+ return workstreamTitleGeneration.enqueue(job, { jobId: `workstream-title:${job.workstreamId}` })
65
31
  }
32
+
33
+ export const startWorkstreamTitleGenerationWorker = workstreamTitleGeneration.startWorker
@@ -58,6 +58,7 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
58
58
  private healthCheckInterval: ReturnType<typeof setInterval> | null = null
59
59
  private isInitialized = false
60
60
  private isClosing = false
61
+ private isHealthCheckRunning = false
61
62
 
62
63
  constructor(private readonly options: CreateRedisConnectionManagerOptions) {
63
64
  this.initializeConnection()
@@ -128,6 +129,8 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
128
129
 
129
130
  const intervalMs = this.options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
130
131
  this.healthCheckInterval = setInterval(async () => {
132
+ if (this.isHealthCheckRunning) return
133
+ this.isHealthCheckRunning = true
131
134
  try {
132
135
  if (this.redis && this.redis.status === 'ready') {
133
136
  await this.redis.ping()
@@ -136,8 +139,11 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
136
139
  } catch (error) {
137
140
  log(this.options.logger, 'warn', `Redis health check failed: ${getErrorMessage(error)}`)
138
141
  this.isHealthy = false
142
+ } finally {
143
+ this.isHealthCheckRunning = false
139
144
  }
140
145
  }, intervalMs)
146
+ this.healthCheckInterval.unref()
141
147
  }
142
148
 
143
149
  getConnection(): IORedis {
@@ -9,7 +9,7 @@ export {
9
9
  } from './connection-accessor'
10
10
  export { withOrgMemoryLock } from './org-memory-lock'
11
11
  export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
12
- export { createWorkstreamResumableContext } from './stream-context'
12
+ export { closeSharedSubscriber, createWorkstreamResumableContext } from './stream-context'
13
13
 
14
14
  export { createRedisConnectionManager }
15
15
  export type { RedisConnectionManager }
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto'
2
1
  import { setTimeout as delay } from 'node:timers/promises'
3
2
 
4
3
  import type IORedis from 'ioredis'
@@ -147,7 +146,7 @@ export async function withRedisLeaseLock<T>(
147
146
  const heldInfoThresholdMs = options.heldInfoThresholdMs ?? 5_000
148
147
  const label = options.label ?? 'redis lease lock'
149
148
 
150
- const lockValue = randomUUID()
149
+ const lockValue = crypto.randomUUID()
151
150
  const waitStart = Date.now()
152
151
  await acquireLeaseLock({ ...options, lockKey, lockValue, label, retryDelayMs, waitLogIntervalMs, maxWaitMs })
153
152
  const waitedMs = Date.now() - waitStart
@@ -44,6 +44,17 @@ function getSharedSubscriber(): Subscriber {
44
44
  return sharedSubscriber.subscriber
45
45
  }
46
46
 
47
+ export async function closeSharedSubscriber(): Promise<void> {
48
+ if (!sharedSubscriber) return
49
+ const { client } = sharedSubscriber
50
+ sharedSubscriber = undefined
51
+ try {
52
+ await client.quit()
53
+ } catch {
54
+ client.disconnect()
55
+ }
56
+ }
57
+
47
58
  export function createWorkstreamResumableContext() {
48
59
  const redis = getRedisConnection()
49
60
  return createResumableStreamContext({