@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
package/src/db/startup.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { recordIdSchema } from '@lota-sdk/shared'
1
2
  import { BoundQuery, RecordId } from 'surrealdb'
2
3
  import { z } from 'zod'
3
4
 
@@ -11,11 +12,11 @@ const DEFAULT_MAX_WAIT_MS = 3 * 60 * 1_000
11
12
  const RETRY_LOG_INTERVAL = 5
12
13
 
13
14
  const RuntimeBootstrapRecordSchema = z.object({
14
- id: z.unknown(),
15
+ id: recordIdSchema,
15
16
  key: z.string(),
16
17
  schemaFingerprint: z.string(),
17
- readyAt: z.union([z.date(), z.string(), z.number()]),
18
- updatedAt: z.union([z.date(), z.string(), z.number()]),
18
+ readyAt: z.coerce.date(),
19
+ updatedAt: z.coerce.date().optional().nullable(),
19
20
  })
20
21
 
21
22
  type StartupLogger = Pick<SurrealDatabaseLogger, 'info' | 'warn' | 'error'>
@@ -62,7 +63,7 @@ export async function connectWithStartupRetry(params: {
62
63
  async function readDatabaseBootstrapRecord(
63
64
  databaseService: SurrealDBService,
64
65
  ): Promise<z.infer<typeof RuntimeBootstrapRecordSchema> | null> {
65
- return await databaseService.queryOne(
66
+ return databaseService.queryOne(
66
67
  new BoundQuery(
67
68
  `SELECT *
68
69
  FROM ${TABLES.RUNTIME_BOOTSTRAP}
@@ -141,12 +142,7 @@ export async function publishDatabaseBootstrap(params: {
141
142
  await params.databaseService.upsert(
142
143
  TABLES.RUNTIME_BOOTSTRAP,
143
144
  new RecordId(TABLES.RUNTIME_BOOTSTRAP, DATABASE_BOOTSTRAP_KEY),
144
- {
145
- key: DATABASE_BOOTSTRAP_KEY,
146
- schemaFingerprint: params.schemaFingerprint,
147
- readyAt: new Date(),
148
- updatedAt: new Date(),
149
- },
145
+ { key: DATABASE_BOOTSTRAP_KEY, schemaFingerprint: params.schemaFingerprint, readyAt: new Date() },
150
146
  RuntimeBootstrapRecordSchema,
151
147
  )
152
148
  }
@@ -0,0 +1,43 @@
1
+ export type SurrealSetClause = { bindingEntries: Record<string, unknown>; statement: string }
2
+
3
+ export function buildRequiredSurrealSetClause(field: string, bindingName: string, value: unknown): SurrealSetClause {
4
+ return { statement: `${field} = $${bindingName}`, bindingEntries: { [bindingName]: value } }
5
+ }
6
+
7
+ export function buildOptionalSurrealSetClause(field: string, value: unknown): SurrealSetClause
8
+ export function buildOptionalSurrealSetClause(field: string, bindingName: string, value: unknown): SurrealSetClause
9
+ export function buildOptionalSurrealSetClause(
10
+ field: string,
11
+ bindingNameOrValue: unknown,
12
+ value?: unknown,
13
+ ): SurrealSetClause {
14
+ const hasExplicitBinding = arguments.length === 3
15
+ const resolvedValue = hasExplicitBinding ? value : bindingNameOrValue
16
+ const resolvedBindingName = hasExplicitBinding
17
+ ? (bindingNameOrValue as string)
18
+ : `resource_${field.replace(/[^a-zA-Z0-9]+/g, '_')}`
19
+
20
+ if (resolvedValue === null || resolvedValue === undefined) {
21
+ return { statement: `${field} = NONE`, bindingEntries: {} }
22
+ }
23
+
24
+ return buildRequiredSurrealSetClause(field, resolvedBindingName, resolvedValue)
25
+ }
26
+
27
+ export function combineSurrealSetClauses(...clauses: SurrealSetClause[]): {
28
+ bindings: Record<string, unknown>
29
+ statement: string
30
+ } {
31
+ const bindings: Record<string, unknown> = {}
32
+ for (const clause of clauses) {
33
+ for (const [key, value] of Object.entries(clause.bindingEntries)) {
34
+ bindings[key] = value
35
+ }
36
+ }
37
+
38
+ return { bindings, statement: clauses.map((clause) => clause.statement).join(', ') }
39
+ }
40
+
41
+ export function compactDefinedRecord<T extends Record<string, unknown>>(value: T): Partial<T> {
42
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>
43
+ }
package/src/db/tables.ts CHANGED
@@ -17,11 +17,18 @@ export const TABLES = {
17
17
  PLAN_CHECKPOINT: 'planCheckpoint',
18
18
  PLAN_VALIDATION_ISSUE: 'planValidationIssue',
19
19
  PLAN_EVENT: 'planEvent',
20
+ PLAN_SCHEDULE: 'planSchedule',
21
+ PLAN_TEMPLATE: 'planTemplate',
22
+ PLAN_CYCLE: 'planCycle',
20
23
  ORGANIZATION: 'organization',
21
24
  ORGANIZATION_MEMBER: 'organizationMember',
22
25
  USER: 'user',
23
26
  RECENT_ACTIVITY_EVENT: 'recentActivityEvent',
24
27
  RECENT_ACTIVITY: 'recentActivity',
28
+ PLAYBOOK: 'playbook',
29
+ PLAYBOOK_VERSION: 'playbookVersion',
30
+ INSTITUTIONAL_MEMORY: 'institutionalMemory',
31
+ QUALITY_METRIC: 'qualityMetric',
25
32
  } as const
26
33
 
27
34
  export type DatabaseTable = (typeof TABLES)[keyof typeof TABLES] | (string & {})
@@ -0,0 +1,15 @@
1
+ import { recordIdSchema } from '@lota-sdk/shared'
2
+ import { z } from 'zod'
3
+
4
+ export const WorkstreamMessageRowSchema = z.object({
5
+ id: recordIdSchema,
6
+ workstreamId: recordIdSchema,
7
+ messageId: z.string(),
8
+ role: z.enum(['system', 'user', 'assistant']),
9
+ parts: z.array(z.record(z.string(), z.unknown())).optional(),
10
+ metadata: z.record(z.string(), z.unknown()).nullish(),
11
+ createdAt: z.coerce.date(),
12
+ updatedAt: z.coerce.date().optional(),
13
+ })
14
+
15
+ export type WorkstreamMessageRow = z.infer<typeof WorkstreamMessageRowSchema>
@@ -71,7 +71,7 @@ export class ProviderEmbeddings {
71
71
  const redisCache = this.getCache()
72
72
  if (!redisCache) return null
73
73
 
74
- return await redisCache.get(this.getModelId(), text)
74
+ return redisCache.get(this.getModelId(), text)
75
75
  }
76
76
 
77
77
  async embedQuery(text: string): Promise<number[]> {
@@ -1,21 +1,11 @@
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
5
  import { TABLES } from '../db/tables'
8
- import { getRedisConnectionForBullMQ } from '../redis'
9
6
  import { contextCompactionService } from '../services/context-compaction.service'
10
7
  import { workstreamService } from '../services/workstream.service'
11
- import {
12
- attachWorkerEvents,
13
- createTracedWorkerProcessor,
14
- createWorkerShutdown,
15
- DEFAULT_JOB_RETENTION,
16
- registerShutdownSignals,
17
- } from '../workers/worker-utils'
18
- import type { WorkerHandle } from '../workers/worker-utils'
8
+ import { createQueueFactory } from './queue-factory'
19
9
 
20
10
  interface ContextCompactionJob {
21
11
  domain: 'workstream'
@@ -23,25 +13,6 @@ interface ContextCompactionJob {
23
13
  contextSize?: number
24
14
  }
25
15
 
26
- const CONTEXT_COMPACTION_QUEUE = 'context-compaction'
27
-
28
- let _contextCompactionQueue: Queue<ContextCompactionJob> | null = null
29
- function getContextCompactionQueue(): Queue<ContextCompactionJob> {
30
- if (!_contextCompactionQueue) {
31
- _contextCompactionQueue = new Queue<ContextCompactionJob>(CONTEXT_COMPACTION_QUEUE, {
32
- connection: getRedisConnectionForBullMQ(),
33
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 3_000 } },
34
- })
35
- }
36
- return _contextCompactionQueue
37
- }
38
-
39
- export async function enqueueContextCompaction(job: ContextCompactionJob) {
40
- return await getContextCompactionQueue().add('compact', job, {
41
- deduplication: { id: `compact:${job.domain}:${job.entityId}` },
42
- })
43
- }
44
-
45
16
  async function processContextCompactionJob(job: Job<ContextCompactionJob>): Promise<void> {
46
17
  await databaseService.connect()
47
18
 
@@ -55,24 +26,22 @@ async function processContextCompactionJob(job: Job<ContextCompactionJob>): Prom
55
26
  }
56
27
  }
57
28
 
58
- export function startContextCompactionWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
59
- const { registerSignals = import.meta.main } = options
60
- const worker = new Worker(
61
- CONTEXT_COMPACTION_QUEUE,
62
- createTracedWorkerProcessor(CONTEXT_COMPACTION_QUEUE, processContextCompactionJob),
63
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 300_000 },
64
- )
65
-
66
- attachWorkerEvents(worker, 'Context compaction', serverLogger)
67
- const shutdown = createWorkerShutdown(worker, 'Context compaction', serverLogger)
68
-
69
- if (registerSignals) {
70
- registerShutdownSignals({ name: 'Context compaction', shutdown, logger: serverLogger })
71
- }
72
-
73
- return { worker, shutdown }
29
+ const contextCompaction = createQueueFactory<ContextCompactionJob>({
30
+ name: 'context-compaction',
31
+ displayName: 'Context compaction',
32
+ jobName: 'compact',
33
+ concurrency: 2,
34
+ lockDuration: 300_000,
35
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 3_000 } },
36
+ processor: processContextCompactionJob,
37
+ })
38
+
39
+ export function enqueueContextCompaction(job: ContextCompactionJob) {
40
+ return contextCompaction.enqueue(job, { deduplication: { id: `compact:${job.domain}:${job.entityId}` } })
74
41
  }
75
42
 
43
+ export const startContextCompactionWorker = contextCompaction.startWorker
44
+
76
45
  if (import.meta.main) {
77
46
  startContextCompactionWorker()
78
47
  }
@@ -0,0 +1,41 @@
1
+ import type { Job } from 'bullmq'
2
+
3
+ import { databaseService } from '../db/service'
4
+ import { planExecutorService } from '../services/plan-executor.service'
5
+ import { createQueueFactory } from './queue-factory'
6
+
7
+ export interface DelayedNodePromotionJob {
8
+ runId: string
9
+ nodeId: string
10
+ emittedBy: string
11
+ }
12
+
13
+ export const DELAYED_NODE_PROMOTION_QUEUE = 'delayed-node-promotion'
14
+
15
+ async function processDelayedNodePromotionJob(job: Job<DelayedNodePromotionJob>): Promise<void> {
16
+ await databaseService.connect()
17
+ await planExecutorService.promoteDelayedNode({
18
+ runId: job.data.runId,
19
+ nodeId: job.data.nodeId,
20
+ emittedBy: job.data.emittedBy,
21
+ })
22
+ }
23
+
24
+ const delayedNodePromotion = createQueueFactory<DelayedNodePromotionJob>({
25
+ name: DELAYED_NODE_PROMOTION_QUEUE,
26
+ displayName: 'Delayed node promotion',
27
+ jobName: 'promote-node',
28
+ concurrency: 1,
29
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
30
+ processor: processDelayedNodePromotionJob,
31
+ })
32
+
33
+ export async function enqueueDelayedNodePromotion(job: DelayedNodePromotionJob, delayMs: number) {
34
+ await delayedNodePromotion.enqueue(job, { delay: delayMs, jobId: `promote:${job.runId}:${job.nodeId}` })
35
+ }
36
+
37
+ export const startDelayedNodePromotionWorker = delayedNodePromotion.startWorker
38
+
39
+ if (import.meta.main) {
40
+ startDelayedNodePromotionWorker()
41
+ }
@@ -1,5 +1,3 @@
1
- import { createHash } from 'node:crypto'
2
-
3
1
  import { Queue, Worker } from 'bullmq'
4
2
  import type IORedis from 'ioredis'
5
3
 
@@ -43,7 +41,7 @@ export function buildDocumentProcessorJobId(
43
41
  'orgId' | 'source' | 'sourceId' | 'sourceCanonicalKey' | 'sourceVersionKey' | 'title'
44
42
  >,
45
43
  ): string {
46
- const digest = createHash('sha256')
44
+ const digest = new Bun.CryptoHasher('sha256')
47
45
  .update(
48
46
  JSON.stringify({
49
47
  orgId: job.orgId,
@@ -73,7 +71,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
73
71
  } {
74
72
  const queueName = params.queueName ?? DEFAULT_DOCUMENT_PROCESSOR_QUEUE
75
73
  const workerName = params.workerName ?? DEFAULT_WORKER_NAME
76
- const concurrency = params.concurrency ?? 4
74
+ const concurrency = params.concurrency ?? 10
77
75
  const lockDuration = params.lockDuration ?? 300_000
78
76
  const jobName = 'process-document' as Parameters<Queue<TJob, unknown, string>['add']>[0]
79
77
  const toQueueData = (job: TJob): Parameters<Queue<TJob, unknown, string>['add']>[1] =>
@@ -1,6 +1,9 @@
1
+ export * from './queue-factory'
1
2
  export * from './context-compaction.queue'
3
+ export * from './delayed-node-promotion.queue'
2
4
  export * from './document-processor.queue'
3
5
  export * from './memory-consolidation.queue'
6
+ export * from './plan-scheduler.queue'
4
7
  export * from './post-chat-memory.queue'
5
8
  export * from './recent-activity-title-refinement.queue'
6
9
  export * from './regular-chat-memory-digest.config'
@@ -1,69 +1,34 @@
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
- LONG_JOB_LOCK_DURATION_MS,
9
- LOW_JOB_RETENTION,
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, LOW_JOB_RETENTION } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
14
3
 
15
4
  export interface MemoryConsolidationJob {
16
5
  scopeId?: string
17
6
  }
18
7
 
19
- const MEMORY_CONSOLIDATION_QUEUE = 'memory-consolidation'
20
8
  const MEMORY_CONSOLIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000
21
9
  const MEMORY_CONSOLIDATION_JOB_ID = 'memory-consolidation-recurring'
22
10
 
23
- let _memoryConsolidationQueue: Queue<MemoryConsolidationJob> | null = null
24
- function getMemoryConsolidationQueue(): Queue<MemoryConsolidationJob> {
25
- if (!_memoryConsolidationQueue) {
26
- _memoryConsolidationQueue = new Queue<MemoryConsolidationJob>(MEMORY_CONSOLIDATION_QUEUE, {
27
- connection: getRedisConnectionForBullMQ(),
28
- defaultJobOptions: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
29
- })
30
- }
31
- return _memoryConsolidationQueue
32
- }
11
+ const memoryConsolidation = createQueueFactory<MemoryConsolidationJob>({
12
+ name: 'memory-consolidation',
13
+ displayName: 'Memory consolidation',
14
+ jobName: 'consolidate-turn',
15
+ concurrency: 1,
16
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
17
+ defaultJobOptions: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
18
+ processorPath: getWorkerPath('memory-consolidation.worker.ts'),
19
+ })
33
20
 
34
21
  export async function enqueueMemoryConsolidation(job: MemoryConsolidationJob = {}) {
35
- await getMemoryConsolidationQueue().add('consolidate-turn', job, {
36
- jobId: job.scopeId ? `consolidate-turn:${job.scopeId}` : undefined,
37
- })
22
+ await memoryConsolidation.enqueue(job, { jobId: job.scopeId ? `consolidate-turn:${job.scopeId}` : undefined })
38
23
  }
39
24
 
40
25
  export async function scheduleRecurringConsolidation() {
41
- await getMemoryConsolidationQueue().add(
42
- 'consolidate',
43
- {},
44
- { repeat: { every: MEMORY_CONSOLIDATION_INTERVAL_MS }, jobId: MEMORY_CONSOLIDATION_JOB_ID },
45
- )
26
+ await memoryConsolidation
27
+ .getQueue()
28
+ .add('consolidate', {}, { repeat: { every: MEMORY_CONSOLIDATION_INTERVAL_MS }, jobId: MEMORY_CONSOLIDATION_JOB_ID })
46
29
  }
47
30
 
48
- export function startMemoryConsolidationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const processorPath = getWorkerPath('memory-consolidation.worker.ts')
51
- const worker = new Worker(MEMORY_CONSOLIDATION_QUEUE, processorPath, {
52
- connection: getRedisConnectionForBullMQ(),
53
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
54
- concurrency: 1,
55
- })
56
-
57
- attachWorkerEvents(worker, 'Memory consolidation', serverLogger)
58
-
59
- const shutdown = createWorkerShutdown(worker, 'Memory consolidation', serverLogger)
60
-
61
- if (registerSignals) {
62
- registerShutdownSignals({ name: 'Memory consolidation', shutdown, logger: serverLogger })
63
- }
64
-
65
- return { worker, shutdown }
66
- }
31
+ export const startMemoryConsolidationWorker = memoryConsolidation.startWorker
67
32
 
68
33
  if (import.meta.main) {
69
34
  startMemoryConsolidationWorker()
@@ -0,0 +1,97 @@
1
+ import type { Job } from 'bullmq'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import { databaseService } from '../db/service'
5
+ import { planDeadlineService } from '../services/plan-deadline.service'
6
+ import { planSchedulerService } from '../services/plan-scheduler.service'
7
+ import type { WorkerHandle } from '../workers/worker-utils'
8
+ import { createQueueFactory } from './queue-factory'
9
+
10
+ export interface PlanSchedulerFireJob {
11
+ type: 'fire-schedule'
12
+ scheduleId: string
13
+ }
14
+
15
+ export interface PlanSchedulerDeadlineJob {
16
+ type: 'check-deadlines'
17
+ scheduledFor?: string
18
+ }
19
+
20
+ export type PlanSchedulerJob = PlanSchedulerFireJob | PlanSchedulerDeadlineJob
21
+
22
+ export const PLAN_SCHEDULER_QUEUE = 'plan-scheduler'
23
+
24
+ async function processPlanSchedulerJob(job: Job<PlanSchedulerJob>): Promise<void> {
25
+ await databaseService.connect()
26
+
27
+ switch (job.data.type) {
28
+ case 'fire-schedule':
29
+ await planSchedulerService.fireScheduleById(job.data.scheduleId)
30
+ break
31
+ case 'check-deadlines':
32
+ await planDeadlineService.checkDeadlines()
33
+ break
34
+ }
35
+ }
36
+
37
+ const planScheduler = createQueueFactory<PlanSchedulerJob>({
38
+ name: PLAN_SCHEDULER_QUEUE,
39
+ displayName: 'Plan scheduler',
40
+ jobName: 'plan-scheduler-job',
41
+ concurrency: 1,
42
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
43
+ processor: processPlanSchedulerJob,
44
+ })
45
+
46
+ /** Enqueue a delayed job that fires a specific schedule at its nextFireAt time. */
47
+ export async function enqueueScheduleFire(scheduleId: string, delayMs: number): Promise<void> {
48
+ await planScheduler.enqueue(
49
+ { type: 'fire-schedule', scheduleId },
50
+ { delay: Math.max(0, delayMs), jobId: `schedule:${scheduleId}`, removeOnComplete: true, removeOnFail: 50 },
51
+ )
52
+ }
53
+
54
+ /** Remove a pending fire job for a schedule (on cancel/pause/complete). */
55
+ export async function removeScheduleFireJob(scheduleId: string): Promise<void> {
56
+ try {
57
+ await planScheduler.getQueue().remove(`schedule:${scheduleId}`)
58
+ } catch {
59
+ // Job may not exist (already fired or never enqueued) — safe to ignore
60
+ }
61
+ }
62
+
63
+ const DEADLINE_CHECK_JOB_PREFIX = 'deadline-check'
64
+
65
+ export async function enqueueDeadlineCheck(scheduledFor: Date): Promise<void> {
66
+ const delay = Math.max(0, scheduledFor.getTime() - Date.now())
67
+ await planScheduler.enqueue(
68
+ { type: 'check-deadlines', scheduledFor: scheduledFor.toISOString() },
69
+ {
70
+ delay,
71
+ jobId: `${DEADLINE_CHECK_JOB_PREFIX}:${scheduledFor.getTime()}`,
72
+ removeOnComplete: true,
73
+ removeOnFail: 50,
74
+ },
75
+ )
76
+ }
77
+
78
+ export function startPlanSchedulerWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
79
+ const handle = planScheduler.startWorker(options)
80
+
81
+ // Recover active schedules on startup
82
+ planSchedulerService.recoverActiveSchedules().catch((err: unknown) => {
83
+ serverLogger.error`Plan scheduler startup recovery failed: ${err}`
84
+ })
85
+
86
+ // Seed deadline checks from current runtime state. Subsequent checks are
87
+ // re-seeded by the queue job after each sweep and by schedule fire events.
88
+ planDeadlineService.recoverDeadlineChecks().catch((err: unknown) => {
89
+ serverLogger.error`Plan deadline recovery failed: ${err}`
90
+ })
91
+
92
+ return handle
93
+ }
94
+
95
+ if (import.meta.main) {
96
+ startPlanSchedulerWorker()
97
+ }
@@ -1,19 +1,9 @@
1
1
  type OrganizationOnboardStatus = string
2
- import { Queue, Worker } from 'bullmq'
3
2
  import type { Job } from 'bullmq'
4
3
 
5
- import { serverLogger } from '../config/logger'
6
4
  import { databaseService } from '../db/service'
7
- import { getRedisConnectionForBullMQ } from '../redis'
8
5
  import { memoryService } from '../services/memory.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 PostChatMemoryMessage {
19
9
  role: 'user' | 'agent'
@@ -25,6 +15,8 @@ interface PostChatMemoryExtractionJob {
25
15
  orgId: string
26
16
  workstreamId: string
27
17
  sourceId: string
18
+ source?: string
19
+ sourceMetadata?: Record<string, unknown>
28
20
  onboardStatus?: OrganizationOnboardStatus
29
21
  userMessage: string
30
22
  historyMessages: PostChatMemoryMessage[]
@@ -33,27 +25,6 @@ interface PostChatMemoryExtractionJob {
33
25
  attachmentContext?: string
34
26
  }
35
27
 
36
- const POST_CHAT_MEMORY_QUEUE = 'post-chat-memory'
37
- const POST_CHAT_MEMORY_CONCURRENCY = 3
38
- const POST_CHAT_MEMORY_LOCK_DURATION_MS = 900_000
39
- const POST_CHAT_MEMORY_MAX_STALLED_COUNT = 10
40
- const POST_CHAT_MEMORY_STALLED_INTERVAL_MS = 120_000
41
-
42
- let _postChatMemoryQueue: Queue<PostChatMemoryExtractionJob> | null = null
43
- function getPostChatMemoryQueue(): Queue<PostChatMemoryExtractionJob> {
44
- if (!_postChatMemoryQueue) {
45
- _postChatMemoryQueue = new Queue<PostChatMemoryExtractionJob>(POST_CHAT_MEMORY_QUEUE, {
46
- connection: getRedisConnectionForBullMQ(),
47
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
48
- })
49
- }
50
- return _postChatMemoryQueue
51
- }
52
-
53
- export async function enqueuePostChatMemory(job: PostChatMemoryExtractionJob) {
54
- return await getPostChatMemoryQueue().add('extract-memory', job)
55
- }
56
-
57
28
  async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>): Promise<void> {
58
29
  await databaseService.connect()
59
30
 
@@ -85,6 +56,8 @@ async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>):
85
56
  input: userMessage,
86
57
  output: joinedOutput,
87
58
  sourceId: data.sourceId,
59
+ source: data.source,
60
+ sourceMetadata: data.sourceMetadata,
88
61
  onboardStatus: data.onboardStatus,
89
62
  ...(uniqueAgentNames.length > 0 ? { agentName: uniqueAgentNames[0] } : {}),
90
63
  historyMessages: data.historyMessages,
@@ -94,30 +67,22 @@ async function processPostChatMemoryJob(job: Job<PostChatMemoryExtractionJob>):
94
67
  })
95
68
  }
96
69
 
97
- export function startPostChatMemoryWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
98
- const { registerSignals = import.meta.main } = options
99
- const worker = new Worker(
100
- POST_CHAT_MEMORY_QUEUE,
101
- createTracedWorkerProcessor(POST_CHAT_MEMORY_QUEUE, processPostChatMemoryJob),
102
- {
103
- connection: getRedisConnectionForBullMQ(),
104
- concurrency: POST_CHAT_MEMORY_CONCURRENCY,
105
- lockDuration: POST_CHAT_MEMORY_LOCK_DURATION_MS,
106
- maxStalledCount: POST_CHAT_MEMORY_MAX_STALLED_COUNT,
107
- stalledInterval: POST_CHAT_MEMORY_STALLED_INTERVAL_MS,
108
- },
109
- )
110
-
111
- attachWorkerEvents(worker, 'Post-chat memory', serverLogger)
112
-
113
- const shutdown = createWorkerShutdown(worker, 'Post-chat memory', serverLogger)
114
-
115
- if (registerSignals) {
116
- registerShutdownSignals({ name: 'Post-chat memory', shutdown, logger: serverLogger })
117
- }
118
-
119
- return { worker, shutdown }
70
+ const postChatMemory = createQueueFactory<PostChatMemoryExtractionJob>({
71
+ name: 'post-chat-memory',
72
+ displayName: 'Post-chat memory',
73
+ jobName: 'extract-memory',
74
+ concurrency: 10,
75
+ lockDuration: 900_000,
76
+ maxStalledCount: 10,
77
+ stalledInterval: 120_000,
78
+ defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
79
+ processor: processPostChatMemoryJob,
80
+ })
81
+
82
+ export function enqueuePostChatMemory(job: PostChatMemoryExtractionJob, options?: { dedupeKey?: string }) {
83
+ return postChatMemory.enqueue(job, options?.dedupeKey ? { jobId: options.dedupeKey } : undefined)
120
84
  }
85
+ export const startPostChatMemoryWorker = postChatMemory.startWorker
121
86
 
122
87
  if (import.meta.main) {
123
88
  startPostChatMemoryWorker()