@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.
- package/infrastructure/schema/00_identity.surql +0 -2
- package/infrastructure/schema/01_memory.surql +1 -1
- package/infrastructure/schema/02_execution_plan.surql +62 -1
- package/infrastructure/schema/03_learned_skill.surql +1 -1
- package/infrastructure/schema/06_playbook.surql +25 -0
- package/infrastructure/schema/07_institutional_memory.surql +13 -0
- package/infrastructure/schema/08_quality_metrics.surql +17 -0
- package/package.json +9 -8
- package/src/ai/definitions.ts +80 -2
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +0 -1
- package/src/bifrost/bifrost.ts +14 -14
- package/src/config/agent-defaults.ts +32 -22
- package/src/config/agent-types.ts +11 -0
- package/src/config/constants.ts +2 -14
- package/src/config/debug-logger.ts +5 -1
- package/src/config/index.ts +3 -0
- package/src/config/logger.ts +7 -9
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +453 -0
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.rows.ts +7 -7
- package/src/db/memory-store.ts +24 -24
- package/src/db/memory.ts +18 -16
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +193 -122
- package/src/db/startup.ts +9 -13
- package/src/db/surreal-mutation.ts +43 -0
- package/src/db/tables.ts +7 -0
- package/src/db/workstream-message-row.ts +15 -0
- package/src/embeddings/provider.ts +1 -1
- package/src/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +17 -52
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +18 -54
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +15 -60
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
- package/src/queues/skill-extraction.queue.ts +15 -50
- package/src/queues/workstream-title-generation.queue.ts +15 -51
- package/src/redis/connection.ts +12 -3
- package/src/redis/index.ts +2 -1
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +106 -21
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +7 -5
- package/src/runtime/context-compaction.ts +40 -97
- package/src/runtime/execution-plan.ts +23 -19
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +10 -196
- package/src/runtime/index.ts +14 -1
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +11 -12
- package/src/runtime/memory-pipeline.ts +26 -10
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +73 -1
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +25 -12
- package/src/runtime/runtime-extensions.ts +91 -15
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -28
- package/src/runtime/team-consultation-prompts.ts +11 -2
- package/src/runtime/title-helpers.ts +11 -4
- package/src/runtime/workstream-chat-helpers.ts +6 -7
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- package/src/services/adaptive-playbook.service.ts +152 -0
- package/src/services/agent-executor.service.ts +293 -0
- package/src/services/artifact-provenance.service.ts +172 -0
- package/src/services/attachment.service.ts +7 -12
- package/src/services/context-compaction.service.ts +75 -58
- package/src/services/context-enrichment.service.ts +33 -0
- package/src/services/coordination-registry.service.ts +117 -0
- package/src/services/document-chunk.service.ts +38 -33
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +271 -50
- package/src/services/feedback-loop.service.ts +96 -0
- package/src/services/global-orchestrator.service.ts +148 -0
- package/src/services/index.ts +26 -0
- package/src/services/institutional-memory.service.ts +145 -0
- package/src/services/learned-skill.service.ts +30 -15
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
- package/src/services/memory.service.ts +55 -69
- package/src/services/monitoring-window.service.ts +86 -0
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +155 -0
- package/src/services/notification.service.ts +39 -0
- package/src/services/organization-member.service.ts +12 -5
- package/src/services/organization.service.ts +5 -5
- package/src/services/ownership-dispatcher.service.ts +403 -0
- package/src/services/plan-approval.service.ts +1 -1
- package/src/services/plan-artifact.service.ts +1 -0
- package/src/services/plan-builder.service.ts +1 -0
- package/src/services/plan-checkpoint.service.ts +30 -2
- package/src/services/plan-compiler.service.ts +5 -0
- package/src/services/plan-coordination.service.ts +152 -0
- package/src/services/plan-cycle.service.ts +284 -0
- package/src/services/plan-deadline.service.ts +287 -0
- package/src/services/plan-executor.service.ts +386 -58
- package/src/services/plan-helpers.ts +15 -0
- package/src/services/plan-run.service.ts +41 -7
- package/src/services/plan-scheduler.service.ts +240 -0
- package/src/services/plan-template.service.ts +117 -0
- package/src/services/plan-validator.service.ts +87 -20
- package/src/services/plan-workspace.service.ts +83 -0
- package/src/services/playbook-registry.service.ts +67 -0
- package/src/services/plugin-executor.service.ts +103 -0
- package/src/services/quality-metrics.service.ts +132 -0
- package/src/services/recent-activity-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +33 -43
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +29 -41
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +55 -65
- package/src/services/workstream.types.ts +10 -19
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/storage/index.ts +2 -2
- package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
- package/src/system-agents/delegated-agent-factory.ts +5 -2
- package/src/system-agents/index.ts +8 -0
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +17 -19
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -3
- package/src/tools/read-file-parts.tool.ts +1 -1
- package/src/tools/search-web.tool.ts +18 -15
- package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
- package/src/tools/team-think.tool.ts +14 -8
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +3 -2
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +47 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +19 -2
- package/src/utils/string.ts +128 -1
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/index.ts +1 -0
- package/src/workers/memory-consolidation.worker.ts +12 -12
- package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
- package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
- package/src/workers/skill-extraction.runner.ts +8 -102
- package/src/workers/utils/file-section-chunker.ts +6 -3
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/sandbox-error.ts +11 -2
- package/src/workers/utils/workstream-message-query.ts +97 -0
- package/src/workers/worker-utils.ts +6 -2
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/runtime.ts +0 -387
- package/src/tools/log-hello-world.tool.ts +0 -17
- package/src/utils/error.ts +0 -10
- /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
- /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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
27
|
+
await regularChatMemoryDigest.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
|
|
49
28
|
}
|
|
50
29
|
|
|
51
|
-
export
|
|
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 {
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
package/src/redis/connection.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import IORedis from 'ioredis'
|
|
2
2
|
import type { RedisOptions } from 'ioredis'
|
|
3
3
|
|
|
4
|
-
import { getErrorMessage } from '../utils/
|
|
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 *
|
|
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:
|
|
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 {
|
package/src/redis/index.ts
CHANGED
|
@@ -8,7 +8,8 @@ export {
|
|
|
8
8
|
type RedisConnectionAccessor,
|
|
9
9
|
} from './connection-accessor'
|
|
10
10
|
export { withOrgMemoryLock } from './org-memory-lock'
|
|
11
|
-
export {
|
|
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/
|
|
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
|
-
|
|
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>(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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({
|