@lota-sdk/core 0.1.14 → 0.1.16

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 (174) 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 +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -1,69 +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
- registerShutdownSignals,
13
- } from '../workers/worker-utils'
14
- import type { WorkerHandle } from '../workers/worker-utils'
5
+ import { createQueueFactory } from './queue-factory'
15
6
 
16
7
  interface RecentActivityTitleRefinementJob {
17
8
  activityId: string
18
9
  }
19
10
 
20
- const RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE = 'recent-activity-title-refinement'
21
-
22
- let _recentActivityTitleRefinementQueue: Queue<RecentActivityTitleRefinementJob> | null = null
23
- function getRecentActivityTitleRefinementQueue(): Queue<RecentActivityTitleRefinementJob> {
24
- if (!_recentActivityTitleRefinementQueue) {
25
- _recentActivityTitleRefinementQueue = new Queue<RecentActivityTitleRefinementJob>(
26
- RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE,
27
- {
28
- connection: getRedisConnectionForBullMQ(),
29
- defaultJobOptions: {
30
- removeOnComplete: 200,
31
- removeOnFail: 200,
32
- attempts: 3,
33
- backoff: { type: 'exponential', delay: 2_000 },
34
- },
35
- },
36
- )
37
- }
38
- return _recentActivityTitleRefinementQueue
39
- }
40
-
41
- export async function enqueueRecentActivityTitleRefinement(job: RecentActivityTitleRefinementJob) {
42
- return await getRecentActivityTitleRefinementQueue().add('refine-recent-activity-title', job, {
43
- jobId: `recent-activity-title:${job.activityId}`,
44
- })
45
- }
46
-
47
11
  async function processRecentActivityTitleRefinementJob(job: Job<RecentActivityTitleRefinementJob>): Promise<void> {
48
12
  await databaseService.connect()
49
13
  await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
50
14
  }
51
15
 
52
- export function startRecentActivityTitleRefinementWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
53
- const { registerSignals = import.meta.main } = options
54
- const worker = new Worker(
55
- RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE,
56
- createTracedWorkerProcessor(RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE, processRecentActivityTitleRefinementJob),
57
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 300_000 },
58
- )
59
-
60
- attachWorkerEvents(worker, 'Recent activity title refinement', serverLogger)
61
-
62
- const shutdown = createWorkerShutdown(worker, 'Recent activity title refinement', serverLogger)
63
-
64
- if (registerSignals) {
65
- registerShutdownSignals({ name: 'Recent activity title refinement', shutdown, logger: serverLogger })
66
- }
67
-
68
- 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: 2,
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}` })
69
28
  }
29
+
30
+ export const startRecentActivityTitleRefinementWorker = recentActivityTitleRefinement.startWorker
@@ -1,14 +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
- getWorkerPath,
8
- createWorkerShutdown,
9
- registerShutdownSignals,
10
- } from '../workers/worker-utils'
11
- 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'
12
3
  import {
13
4
  buildRegularChatMemoryDigestDeduplicationId,
14
5
  buildRegularChatMemoryDigestJobOptions,
@@ -18,55 +9,25 @@ export interface RegularChatMemoryDigestJob {
18
9
  orgId: string
19
10
  }
20
11
 
21
- const REGULAR_CHAT_MEMORY_DIGEST_QUEUE = 'regular-chat-memory-digest'
22
-
23
- let _regularChatMemoryDigestQueue: Queue<RegularChatMemoryDigestJob> | null = null
24
- function getRegularChatMemoryDigestQueue(): Queue<RegularChatMemoryDigestJob> {
25
- if (!_regularChatMemoryDigestQueue) {
26
- _regularChatMemoryDigestQueue = new Queue<RegularChatMemoryDigestJob>(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, {
27
- connection: getRedisConnectionForBullMQ(),
28
- defaultJobOptions: {
29
- removeOnComplete: 200,
30
- removeOnFail: 200,
31
- attempts: 2,
32
- backoff: { type: 'exponential', delay: 5000 },
33
- },
34
- })
35
- }
36
- return _regularChatMemoryDigestQueue
37
- }
38
-
39
- export async function enqueueRegularChatMemoryDigest(job: RegularChatMemoryDigestJob) {
40
- return await getRegularChatMemoryDigestQueue().add(
41
- 'run-digest',
42
- job,
43
- buildRegularChatMemoryDigestJobOptions(job.orgId),
44
- )
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))
45
24
  }
46
25
 
47
26
  export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
48
- await getRegularChatMemoryDigestQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
27
+ await regularChatMemoryDigest.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
49
28
  }
50
29
 
51
- export function startRegularChatMemoryDigestWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
52
- const { registerSignals = import.meta.main } = options
53
- const processorPath = getWorkerPath('regular-chat-memory-digest.worker.ts')
54
- const worker = new Worker(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, processorPath, {
55
- connection: getRedisConnectionForBullMQ(),
56
- concurrency: 1,
57
- lockDuration: 600_000,
58
- })
59
-
60
- attachWorkerEvents(worker, 'Regular chat memory digest', serverLogger)
61
-
62
- const shutdown = createWorkerShutdown(worker, 'Regular chat memory digest', serverLogger)
63
-
64
- if (registerSignals) {
65
- registerShutdownSignals({ name: 'Regular chat memory digest', shutdown, logger: serverLogger })
66
- }
67
-
68
- return { worker, shutdown }
69
- }
30
+ export const startRegularChatMemoryDigestWorker = regularChatMemoryDigest.startWorker
70
31
 
71
32
  if (import.meta.main) {
72
33
  startRegularChatMemoryDigestWorker()
@@ -1,61 +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
- getWorkerPath,
8
- createWorkerShutdown,
9
- registerShutdownSignals,
10
- } from '../workers/worker-utils'
11
- import type { WorkerHandle } from '../workers/worker-utils'
1
+ import { getWorkerPath } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
12
3
  import { buildSkillExtractionJobOptions } from './skill-extraction.config'
13
4
 
14
5
  export interface SkillExtractionJob {
15
6
  orgId: string
16
7
  }
17
8
 
18
- const SKILL_EXTRACTION_QUEUE = 'skill-extraction'
19
-
20
- let _skillExtractionQueue: Queue<SkillExtractionJob> | null = null
21
- function getSkillExtractionQueue(): Queue<SkillExtractionJob> {
22
- if (!_skillExtractionQueue) {
23
- _skillExtractionQueue = new Queue<SkillExtractionJob>(SKILL_EXTRACTION_QUEUE, {
24
- connection: getRedisConnectionForBullMQ(),
25
- defaultJobOptions: {
26
- removeOnComplete: 200,
27
- removeOnFail: 200,
28
- attempts: 2,
29
- backoff: { type: 'exponential', delay: 5000 },
30
- },
31
- })
32
- }
33
- return _skillExtractionQueue
9
+ const skillExtraction = createQueueFactory<SkillExtractionJob>({
10
+ name: 'skill-extraction',
11
+ displayName: 'Skill extraction',
12
+ jobName: 'run-extraction',
13
+ concurrency: 1,
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))
34
21
  }
35
22
 
36
- export async function enqueueSkillExtraction(job: SkillExtractionJob) {
37
- return await getSkillExtractionQueue().add('run-extraction', job, buildSkillExtractionJobOptions(job.orgId))
38
- }
39
-
40
- export function startSkillExtractionWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
41
- const { registerSignals = import.meta.main } = options
42
- const processorPath = getWorkerPath('skill-extraction.worker.ts')
43
- const worker = new Worker(SKILL_EXTRACTION_QUEUE, processorPath, {
44
- connection: getRedisConnectionForBullMQ(),
45
- concurrency: 1,
46
- lockDuration: 600_000,
47
- })
48
-
49
- attachWorkerEvents(worker, 'Skill extraction', serverLogger)
50
-
51
- const shutdown = createWorkerShutdown(worker, 'Skill extraction', serverLogger)
52
-
53
- if (registerSignals) {
54
- registerShutdownSignals({ name: 'Skill extraction', shutdown, logger: serverLogger })
55
- }
56
-
57
- return { worker, shutdown }
58
- }
23
+ export const startSkillExtractionWorker = skillExtraction.startWorker
59
24
 
60
25
  if (import.meta.main) {
61
26
  startSkillExtractionWorker()
@@ -1,69 +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
- registerShutdownSignals,
14
- } from '../workers/worker-utils'
15
- import type { WorkerHandle } from '../workers/worker-utils'
6
+ import { createQueueFactory } from './queue-factory'
16
7
 
17
8
  interface WorkstreamTitleGenerationJob {
18
9
  workstreamId: string
19
10
  sourceText: string
20
11
  }
21
12
 
22
- const WORKSTREAM_TITLE_GENERATION_QUEUE = 'workstream-title-generation'
23
-
24
- let _workstreamTitleGenerationQueue: Queue<WorkstreamTitleGenerationJob> | null = null
25
- function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob> {
26
- if (!_workstreamTitleGenerationQueue) {
27
- _workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
28
- connection: getRedisConnectionForBullMQ(),
29
- defaultJobOptions: {
30
- removeOnComplete: 200,
31
- removeOnFail: 200,
32
- attempts: 2,
33
- backoff: { type: 'exponential', delay: 2_000 },
34
- },
35
- })
36
- }
37
- return _workstreamTitleGenerationQueue
38
- }
39
-
40
- export async function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
41
- return await getWorkstreamTitleGenerationQueue().add('generate-workstream-title', job, {
42
- jobId: `workstream-title:${job.workstreamId}`,
43
- })
44
- }
45
-
46
13
  async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
47
14
  await databaseService.connect()
48
15
  const workstreamRef = ensureRecordId(job.data.workstreamId)
49
16
  await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
50
17
  }
51
18
 
52
- export function startWorkstreamTitleGenerationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
53
- const { registerSignals = import.meta.main } = options
54
- const worker = new Worker(
55
- WORKSTREAM_TITLE_GENERATION_QUEUE,
56
- createTracedWorkerProcessor(WORKSTREAM_TITLE_GENERATION_QUEUE, processWorkstreamTitleGenerationJob),
57
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 60_000 },
58
- )
59
-
60
- attachWorkerEvents(worker, 'Workstream title generation', serverLogger)
61
-
62
- const shutdown = createWorkerShutdown(worker, 'Workstream title generation', serverLogger)
63
-
64
- if (registerSignals) {
65
- registerShutdownSignals({ name: 'Workstream title generation', shutdown, logger: serverLogger })
66
- }
67
-
68
- 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: 2,
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}` })
69
31
  }
32
+
33
+ export const startWorkstreamTitleGenerationWorker = workstreamTitleGeneration.startWorker
@@ -1,7 +1,7 @@
1
1
  import IORedis from 'ioredis'
2
2
  import type { RedisOptions } from 'ioredis'
3
3
 
4
- import { getErrorMessage } from '../utils/error'
4
+ import { getErrorMessage } from '../utils/errors'
5
5
 
6
6
  export interface RedisConnectionLogger {
7
7
  debug?: (message: string) => void
@@ -25,20 +25,23 @@ export interface RedisConnectionManager {
25
25
  }
26
26
 
27
27
  const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 30_000
28
+ const REDIS_RETRY_STEP_MS = 50
29
+ const REDIS_RETRY_MAX_DELAY_MS = 2000
30
+ const REDIS_CONNECT_TIMEOUT_MS = 10_000
28
31
 
29
32
  export const DEFAULT_REDIS_OPTIONS: RedisOptions = {
30
33
  maxRetriesPerRequest: null,
31
34
  enableReadyCheck: true,
32
35
  enableOfflineQueue: true,
33
36
  retryStrategy: (times: number) => {
34
- const delay = Math.min(times * 50, 2000)
37
+ const delay = Math.min(times * REDIS_RETRY_STEP_MS, REDIS_RETRY_MAX_DELAY_MS)
35
38
  return delay
36
39
  },
37
40
  reconnectOnError: (err: Error) => {
38
41
  const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNREFUSED', 'EAI_AGAIN']
39
42
  return targetErrors.some((candidate) => err.message.includes(candidate))
40
43
  },
41
- connectTimeout: 10000,
44
+ connectTimeout: REDIS_CONNECT_TIMEOUT_MS,
42
45
  lazyConnect: false,
43
46
  }
44
47
 
@@ -55,6 +58,7 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
55
58
  private healthCheckInterval: ReturnType<typeof setInterval> | null = null
56
59
  private isInitialized = false
57
60
  private isClosing = false
61
+ private isHealthCheckRunning = false
58
62
 
59
63
  constructor(private readonly options: CreateRedisConnectionManagerOptions) {
60
64
  this.initializeConnection()
@@ -125,6 +129,8 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
125
129
 
126
130
  const intervalMs = this.options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
127
131
  this.healthCheckInterval = setInterval(async () => {
132
+ if (this.isHealthCheckRunning) return
133
+ this.isHealthCheckRunning = true
128
134
  try {
129
135
  if (this.redis && this.redis.status === 'ready') {
130
136
  await this.redis.ping()
@@ -133,8 +139,11 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
133
139
  } catch (error) {
134
140
  log(this.options.logger, 'warn', `Redis health check failed: ${getErrorMessage(error)}`)
135
141
  this.isHealthy = false
142
+ } finally {
143
+ this.isHealthCheckRunning = false
136
144
  }
137
145
  }, intervalMs)
146
+ this.healthCheckInterval.unref()
138
147
  }
139
148
 
140
149
  getConnection(): IORedis {
@@ -8,7 +8,8 @@ export {
8
8
  type RedisConnectionAccessor,
9
9
  } from './connection-accessor'
10
10
  export { withOrgMemoryLock } from './org-memory-lock'
11
- export { createWorkstreamResumableContext } from './stream-context'
11
+ export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
12
+ export { closeSharedSubscriber, createWorkstreamResumableContext } from './stream-context'
12
13
 
13
14
  export { createRedisConnectionManager }
14
15
  export type { RedisConnectionManager }
@@ -9,7 +9,7 @@ const ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS = 30_000
9
9
  const ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS = 30_000
10
10
  const ORG_MEMORY_LOCK_MAX_WAIT_MS = 15 * 60 * 1000
11
11
 
12
- export async function withOrgMemoryLock<T>(orgId: string, fn: () => Promise<T>): Promise<T> {
12
+ export async function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
13
13
  const normalizedOrgId = orgId.trim()
14
14
 
15
15
  if (!normalizedOrgId) {
@@ -3,7 +3,7 @@ import { setTimeout as delay } from 'node:timers/promises'
3
3
 
4
4
  import type IORedis from 'ioredis'
5
5
 
6
- import { getErrorMessage } from '../utils/error'
6
+ import { getErrorMessage } from '../utils/errors'
7
7
 
8
8
  interface RedisLeaseLockLogger {
9
9
  debug?: (message: string) => void
@@ -95,16 +95,30 @@ async function releaseLeaseLock(options: RedisLeaseLockOptions & { lockValue: st
95
95
  await options.redis.eval(RELEASE_LOCK_SCRIPT, 1, options.lockKey, options.lockValue)
96
96
  }
97
97
 
98
+ export class LeaseLockLostError extends Error {
99
+ constructor(message: string) {
100
+ super(message)
101
+ this.name = 'LeaseLockLostError'
102
+ }
103
+ }
104
+
98
105
  function startLeaseLockRefreshLoop(
99
106
  options: Required<Pick<RedisLeaseLockOptions, 'refreshIntervalMs'>> &
100
107
  RedisLeaseLockOptions & { lockValue: string; label: string },
108
+ ac: AbortController,
101
109
  ): () => void {
102
110
  let stopped = false
103
111
 
104
112
  const timer = setInterval(() => {
105
113
  if (stopped) return
106
114
  void refreshLeaseLock(options).catch((error: unknown) => {
107
- log(options.logger, 'warn', `Failed to refresh ${options.label} (${options.lockKey}): ${getErrorMessage(error)}`)
115
+ stopped = true
116
+ clearInterval(timer)
117
+ const message = `Failed to refresh ${options.label} (${options.lockKey}): ${getErrorMessage(error)}`
118
+ log(options.logger, 'warn', message)
119
+ if (!ac.signal.aborted) {
120
+ ac.abort(new LeaseLockLostError(message))
121
+ }
108
122
  })
109
123
  }, options.refreshIntervalMs)
110
124
 
@@ -116,7 +130,10 @@ function startLeaseLockRefreshLoop(
116
130
  }
117
131
  }
118
132
 
119
- export async function withRedisLeaseLock<T>(options: RedisLeaseLockOptions, fn: () => Promise<T>): Promise<T> {
133
+ export async function withRedisLeaseLock<T>(
134
+ options: RedisLeaseLockOptions,
135
+ fn: (signal: AbortSignal) => Promise<T>,
136
+ ): Promise<T> {
120
137
  const lockKey = options.lockKey.trim()
121
138
  if (!lockKey) {
122
139
  throw new Error('Redis lease lock requires a non-empty lock key')
@@ -138,17 +155,33 @@ export async function withRedisLeaseLock<T>(options: RedisLeaseLockOptions, fn:
138
155
  log(options.logger, 'info', `Acquired ${label} (${lockKey}) after waiting waitedMs=${waitedMs}`)
139
156
  }
140
157
 
141
- const stopRefreshLoop = startLeaseLockRefreshLoop({ ...options, lockKey, lockValue, label, refreshIntervalMs })
158
+ const ac = new AbortController()
159
+ const stopRefreshLoop = startLeaseLockRefreshLoop({ ...options, lockKey, lockValue, label, refreshIntervalMs }, ac)
142
160
  const holdStart = Date.now()
143
161
 
144
162
  try {
145
- return await fn()
163
+ const abortPromise = new Promise<never>((_, reject) => {
164
+ if (ac.signal.aborted) {
165
+ reject(ac.signal.reason as Error)
166
+ return
167
+ }
168
+ ac.signal.addEventListener(
169
+ 'abort',
170
+ () => {
171
+ reject(ac.signal.reason as Error)
172
+ },
173
+ { once: true },
174
+ )
175
+ })
176
+ return await Promise.race([fn(ac.signal), abortPromise])
146
177
  } finally {
147
178
  stopRefreshLoop()
148
179
  const heldMs = Date.now() - holdStart
149
- await releaseLeaseLock({ ...options, lockKey, lockValue }).catch((error: unknown) => {
150
- log(options.logger, 'warn', `Failed to release ${label} (${lockKey}): ${getErrorMessage(error)}`)
151
- })
180
+ if (!ac.signal.aborted) {
181
+ await releaseLeaseLock({ ...options, lockKey, lockValue }).catch((error: unknown) => {
182
+ log(options.logger, 'warn', `Failed to release ${label} (${lockKey}): ${getErrorMessage(error)}`)
183
+ })
184
+ }
152
185
  if (heldMs >= heldInfoThresholdMs) {
153
186
  log(options.logger, 'info', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
154
187
  } else {
@@ -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({