@lota-sdk/core 0.1.13 → 0.1.15

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 (95) hide show
  1. package/package.json +5 -5
  2. package/src/ai/embedding-cache.ts +7 -6
  3. package/src/ai/index.ts +1 -0
  4. package/src/bifrost/bifrost.ts +12 -7
  5. package/src/config/agent-defaults.ts +1 -1
  6. package/src/config/logger.ts +7 -9
  7. package/src/{runtime.ts → create-runtime.ts} +6 -6
  8. package/src/db/cursor-pagination.ts +1 -1
  9. package/src/db/memory-store.ts +10 -6
  10. package/src/db/memory.ts +6 -4
  11. package/src/db/schema-fingerprint.ts +1 -0
  12. package/src/db/service.ts +45 -51
  13. package/src/db/startup.ts +3 -3
  14. package/src/index.ts +1 -1
  15. package/src/queues/context-compaction.queue.ts +4 -8
  16. package/src/queues/document-processor.queue.ts +7 -7
  17. package/src/queues/memory-consolidation.queue.ts +7 -8
  18. package/src/queues/post-chat-memory.queue.ts +2 -6
  19. package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
  20. package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
  21. package/src/queues/skill-extraction.queue.ts +4 -7
  22. package/src/queues/workstream-title-generation.queue.ts +2 -6
  23. package/src/redis/connection.ts +6 -3
  24. package/src/redis/index.ts +1 -0
  25. package/src/redis/org-memory-lock.ts +1 -1
  26. package/src/redis/redis-lease-lock.ts +41 -8
  27. package/src/runtime/agent-stream-helpers.ts +2 -1
  28. package/src/runtime/context-compaction-constants.ts +1 -1
  29. package/src/runtime/context-compaction-runtime.ts +6 -4
  30. package/src/runtime/context-compaction.ts +19 -38
  31. package/src/runtime/execution-plan.ts +2 -2
  32. package/src/runtime/helper-model.ts +3 -1
  33. package/src/runtime/index.ts +12 -1
  34. package/src/runtime/memory-block.ts +3 -2
  35. package/src/runtime/memory-pipeline.ts +24 -5
  36. package/src/runtime/plugin-types.ts +1 -1
  37. package/src/runtime/runtime-extensions.ts +89 -13
  38. package/src/runtime/title-helpers.ts +11 -2
  39. package/src/runtime/workstream-chat-helpers.ts +5 -6
  40. package/src/runtime/workstream-routing-policy.ts +0 -30
  41. package/src/runtime/workstream-state.ts +17 -7
  42. package/src/services/attachment.service.ts +1 -1
  43. package/src/services/context-compaction.service.ts +3 -3
  44. package/src/services/document-chunk.service.ts +37 -32
  45. package/src/services/execution-plan.service.ts +2 -0
  46. package/src/services/learned-skill.service.ts +6 -10
  47. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
  48. package/src/services/memory.service.ts +21 -18
  49. package/src/services/organization-member.service.ts +1 -1
  50. package/src/services/plan-artifact.service.ts +1 -0
  51. package/src/services/plan-executor.service.ts +2 -18
  52. package/src/services/plan-helpers.ts +15 -0
  53. package/src/services/plan-validator.service.ts +3 -18
  54. package/src/services/recent-activity-title.service.ts +3 -10
  55. package/src/services/recent-activity.service.ts +6 -12
  56. package/src/services/workstream-message.service.ts +26 -16
  57. package/src/services/workstream-title.service.ts +1 -9
  58. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
  59. package/src/services/workstream-turn.ts +2 -2
  60. package/src/services/workstream.service.ts +22 -10
  61. package/src/services/workstream.types.ts +7 -16
  62. package/src/storage/attachment-storage.service.ts +4 -4
  63. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
  64. package/src/storage/index.ts +2 -2
  65. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  66. package/src/system-agents/delegated-agent-factory.ts +3 -2
  67. package/src/system-agents/index.ts +8 -0
  68. package/src/system-agents/memory-reranker.agent.ts +1 -1
  69. package/src/system-agents/memory.agent.ts +1 -1
  70. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  71. package/src/tools/execution-plan.tool.ts +6 -2
  72. package/src/tools/fetch-webpage.tool.ts +20 -18
  73. package/src/tools/index.ts +2 -2
  74. package/src/tools/read-file-parts.tool.ts +1 -1
  75. package/src/tools/search-web.tool.ts +18 -15
  76. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  77. package/src/tools/team-think.tool.ts +9 -5
  78. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  79. package/src/utils/async.ts +1 -1
  80. package/src/utils/errors.ts +15 -0
  81. package/src/utils/hono-error-handler.ts +1 -2
  82. package/src/utils/index.ts +10 -2
  83. package/src/utils/string.ts +14 -0
  84. package/src/workers/bootstrap.ts +2 -2
  85. package/src/workers/memory-consolidation.worker.ts +12 -12
  86. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  87. package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
  88. package/src/workers/skill-extraction.runner.ts +7 -101
  89. package/src/workers/utils/file-section-chunker.ts +5 -3
  90. package/src/workers/utils/workstream-message-query.ts +106 -0
  91. package/src/workers/worker-utils.ts +4 -0
  92. package/src/runtime/retrieval-pipeline.ts +0 -3
  93. package/src/utils/error.ts +0 -10
  94. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  95. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
package/src/db/service.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
13
13
  import type { z } from 'zod'
14
14
 
15
+ import { withTimeout } from '../utils/async'
16
+ import { isRecord } from '../utils/string'
15
17
  import type { RecordIdInput } from './record-id'
16
18
  import { ensureRecordId, readCustomStringValue } from './record-id'
17
19
  import type { DatabaseTable } from './tables'
@@ -53,11 +55,12 @@ interface FindManyOptions {
53
55
  type MutationBuilder = {
54
56
  content: (data: Record<string, unknown>) => MutationBuilder
55
57
  replace: (data: Record<string, unknown>) => MutationBuilder
56
- patch: (data: Record<string, unknown>) => MutationBuilder
57
58
  merge: (data: Record<string, unknown>) => MutationBuilder
58
59
  output: (mode: 'after' | 'before') => Promise<unknown>
59
60
  }
60
61
 
62
+ type RecordMutation = Extract<Mutation, 'content' | 'replace' | 'merge'>
63
+
61
64
  export type CreateMutationBuilder = {
62
65
  content: (data: Record<string, unknown>) => CreateMutationBuilder
63
66
  output: (mode: 'after' | 'before') => Promise<unknown>
@@ -75,7 +78,7 @@ export interface DatabaseTransaction {
75
78
 
76
79
  function configureMutation(
77
80
  builder: MutationBuilder,
78
- mutation: Mutation,
81
+ mutation: RecordMutation,
79
82
  data: Record<string, unknown>,
80
83
  ): MutationBuilder {
81
84
  if (mutation === 'content') {
@@ -84,18 +87,11 @@ function configureMutation(
84
87
  if (mutation === 'replace') {
85
88
  return builder.replace(data)
86
89
  }
87
- if (mutation === 'patch') {
88
- return builder.patch(data)
89
- }
90
90
  return builder.merge(data)
91
91
  }
92
92
 
93
- function isRecordValue(value: unknown): value is Record<string, unknown> {
94
- return typeof value === 'object' && value !== null && !Array.isArray(value)
95
- }
96
-
97
93
  function isBoundQueryLike(value: unknown): value is BoundQueryLike {
98
- if (!isRecordValue(value)) {
94
+ if (!isRecord(value)) {
99
95
  return false
100
96
  }
101
97
 
@@ -103,7 +99,7 @@ function isBoundQueryLike(value: unknown): value is BoundQueryLike {
103
99
  return false
104
100
  }
105
101
 
106
- return value.bindings === undefined || isRecordValue(value.bindings)
102
+ return value.bindings === undefined || isRecord(value.bindings)
107
103
  }
108
104
 
109
105
  function toStringLikeValue(value: unknown): string | null {
@@ -124,23 +120,6 @@ const CONNECT_RETRY_BASE_DELAY_MS = 100
124
120
  const CONNECT_RETRY_JITTER_MS = 50
125
121
  const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
126
122
 
127
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
128
- let timeoutId: ReturnType<typeof setTimeout> | undefined
129
-
130
- try {
131
- return await Promise.race([
132
- promise,
133
- new Promise<T>((_, reject) => {
134
- timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs)
135
- }),
136
- ])
137
- } finally {
138
- if (timeoutId) {
139
- clearTimeout(timeoutId)
140
- }
141
- }
142
- }
143
-
144
123
  export class SurrealDBService {
145
124
  private client: Surreal | null = null
146
125
  private isConnected = false
@@ -254,7 +233,7 @@ export class SurrealDBService {
254
233
  : { username: this.config.username ?? '', password: this.config.password ?? '' },
255
234
  }),
256
235
  CONNECT_ATTEMPT_TIMEOUT_MS,
257
- `Timed out connecting to SurrealDB at ${this.config.url}`,
236
+ `SurrealDB connect (${this.config.url})`,
258
237
  )
259
238
 
260
239
  this.isConnected = true
@@ -280,7 +259,7 @@ export class SurrealDBService {
280
259
  }
281
260
  }
282
261
 
283
- this.toSurrealError(lastError)
262
+ return this.toSurrealError(lastError)
284
263
  })()
285
264
 
286
265
  try {
@@ -329,8 +308,16 @@ export class SurrealDBService {
329
308
 
330
309
  private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
331
310
  try {
332
- return ensureRecordId(id as RecordIdInput, table)
311
+ const recordId = ensureRecordId(id as RecordIdInput, table)
312
+ const resolvedTable = String(recordId.table)
313
+ if (resolvedTable !== table) {
314
+ throw new SurrealDBError(`Record id table mismatch: expected "${table}" but got "${resolvedTable}"`)
315
+ }
316
+ return recordId
333
317
  } catch (error) {
318
+ if (error instanceof SurrealDBError) {
319
+ throw error
320
+ }
334
321
  if (error instanceof Error) {
335
322
  throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
336
323
  cause: error,
@@ -394,7 +381,7 @@ export class SurrealDBService {
394
381
  return value.map((entry) => this.normalizeRuntimeValue(entry))
395
382
  }
396
383
 
397
- if (!isRecordValue(value)) {
384
+ if (!isRecord(value)) {
398
385
  return value
399
386
  }
400
387
 
@@ -415,6 +402,8 @@ export class SurrealDBService {
415
402
  return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
416
403
  }
417
404
 
405
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
406
+ // (non-array objects are mapped entry-by-entry and returned as Object.fromEntries)
418
407
  private normalizeBindings(bindings?: Record<string, unknown>): Record<string, unknown> | undefined {
419
408
  if (!bindings) {
420
409
  return undefined
@@ -423,6 +412,7 @@ export class SurrealDBService {
423
412
  return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
424
413
  }
425
414
 
415
+ // Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
426
416
  private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
427
417
  const normalized = this.normalizeRuntimeValue(data) as Record<string, unknown>
428
418
  return this.stripNullValues(normalized)
@@ -496,7 +486,6 @@ export class SurrealDBService {
496
486
  return {
497
487
  content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
498
488
  replace: (data) => this.wrapMutationBuilder(builder.replace(this.normalizeMutationData(data))),
499
- patch: (data) => this.wrapMutationBuilder(builder.patch(this.normalizeMutationData(data))),
500
489
  merge: (data) => this.wrapMutationBuilder(builder.merge(this.normalizeMutationData(data))),
501
490
  output: (mode) => builder.output(mode),
502
491
  }
@@ -515,9 +504,11 @@ export class SurrealDBService {
515
504
  create: (target: unknown) => {
516
505
  const normalizedTarget = this.normalizeCreateTarget(target)
517
506
  const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
507
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal CreateMutationBuilder interface
518
508
  return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
519
509
  },
520
510
  update: (target: unknown) =>
511
+ // Cast needed: SurrealDB SDK transaction builder type differs nominally from internal MutationBuilder interface
521
512
  this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
522
513
  delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
523
514
  relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
@@ -559,7 +550,7 @@ export class SurrealDBService {
559
550
  return this.normalizeQueryRows<T>(response.result, schema)
560
551
  })
561
552
  } catch (error) {
562
- this.toSurrealError(error, queryText)
553
+ return this.toSurrealError(error, queryText)
563
554
  }
564
555
  }
565
556
 
@@ -607,7 +598,7 @@ export class SurrealDBService {
607
598
  const first = rows.at(0)
608
599
  return first ? schema.parse(first) : null
609
600
  } catch (error) {
610
- this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
601
+ return this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
611
602
  }
612
603
  }
613
604
 
@@ -659,7 +650,7 @@ export class SurrealDBService {
659
650
  const rows = await query
660
651
  return rows.map((row) => schema.parse(row))
661
652
  } catch (error) {
662
- this.toSurrealError(error, `SELECT * FROM ${table}`)
653
+ return this.toSurrealError(error, `SELECT * FROM ${table}`)
663
654
  }
664
655
  }
665
656
 
@@ -688,7 +679,7 @@ export class SurrealDBService {
688
679
 
689
680
  return schema.parse(first)
690
681
  } catch (error) {
691
- this.toSurrealError(error, `CREATE ${table}`)
682
+ return this.toSurrealError(error, `CREATE ${table}`)
692
683
  }
693
684
  }
694
685
 
@@ -710,7 +701,7 @@ export class SurrealDBService {
710
701
  const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
711
702
  return schema.parse(created)
712
703
  } catch (error) {
713
- this.toSurrealError(error, `CREATE ${recordId.toString()}`)
704
+ return this.toSurrealError(error, `CREATE ${recordId.toString()}`)
714
705
  }
715
706
  }
716
707
 
@@ -719,7 +710,7 @@ export class SurrealDBService {
719
710
  id: unknown,
720
711
  data: Record<string, unknown>,
721
712
  schema: T,
722
- options?: { mutation?: Mutation },
713
+ options?: { mutation?: RecordMutation },
723
714
  ): Promise<z.infer<T> | null> {
724
715
  const recordId = this.normalizeRecordId(id, table)
725
716
 
@@ -737,7 +728,7 @@ export class SurrealDBService {
737
728
  const updated = await configured.output('after')
738
729
  return updated ? schema.parse(updated) : null
739
730
  } catch (error) {
740
- this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
731
+ return this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
741
732
  }
742
733
  }
743
734
 
@@ -746,7 +737,7 @@ export class SurrealDBService {
746
737
  id: unknown,
747
738
  data: Record<string, unknown>,
748
739
  schema: T,
749
- options?: { mutation?: Mutation },
740
+ options?: { mutation?: RecordMutation },
750
741
  ): Promise<z.infer<T>> {
751
742
  const recordId = this.normalizeRecordId(id, table)
752
743
  const keys = Object.keys(data)
@@ -766,7 +757,7 @@ export class SurrealDBService {
766
757
  }
767
758
  return schema.parse(upserted)
768
759
  } catch (error) {
769
- this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
760
+ return this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
770
761
  }
771
762
  }
772
763
 
@@ -782,7 +773,7 @@ export class SurrealDBService {
782
773
  await client.delete<unknown>(recordId).output('before')
783
774
  return true
784
775
  } catch (error) {
785
- this.toSurrealError(error, `DELETE ${recordId.toString()}`)
776
+ return this.toSurrealError(error, `DELETE ${recordId.toString()}`)
786
777
  }
787
778
  }
788
779
 
@@ -812,7 +803,7 @@ export class SurrealDBService {
812
803
 
813
804
  return matched.length
814
805
  } catch (error) {
815
- this.toSurrealError(error, `DELETE ${table} WHERE ...`)
806
+ return this.toSurrealError(error, `DELETE ${table} WHERE ...`)
816
807
  }
817
808
  }
818
809
 
@@ -838,7 +829,7 @@ export class SurrealDBService {
838
829
  }
839
830
  return 1
840
831
  } catch (error) {
841
- this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
832
+ return this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
842
833
  }
843
834
  }
844
835
 
@@ -857,7 +848,7 @@ export class SurrealDBService {
857
848
  .output('after')
858
849
  return inserted as T[]
859
850
  } catch (error) {
860
- this.toSurrealError(error, `INSERT ${table}`)
851
+ return this.toSurrealError(error, `INSERT ${table}`)
861
852
  }
862
853
  }
863
854
 
@@ -887,7 +878,7 @@ export class SurrealDBService {
887
878
  }
888
879
  return related
889
880
  } catch (error) {
890
- this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
881
+ return this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
891
882
  }
892
883
  }
893
884
 
@@ -896,7 +887,7 @@ export class SurrealDBService {
896
887
  try {
897
888
  return this.wrapTransaction(await client.beginTransaction())
898
889
  } catch (error) {
899
- this.toSurrealError(error, 'BEGIN TRANSACTION')
890
+ return this.toSurrealError(error, 'BEGIN TRANSACTION')
900
891
  }
901
892
  }
902
893
 
@@ -945,10 +936,13 @@ export const databaseService = new Proxy({} as SurrealDBService, {
945
936
  return databaseServiceOverrides.get(property)
946
937
  }
947
938
 
948
- const target = resolveConfiguredDatabaseService() as unknown as Record<PropertyKey, unknown>
949
- const value = target[property]
939
+ const resolved = resolveConfiguredDatabaseService()
940
+ const value: unknown = Reflect.get(resolved, property)
950
941
  if (typeof value === 'function') {
951
- return bindTargetMethod(target, value as (...args: unknown[]) => unknown)
942
+ return bindTargetMethod(
943
+ resolved as unknown as Record<PropertyKey, unknown>,
944
+ value as (...args: unknown[]) => unknown,
945
+ )
952
946
  }
953
947
 
954
948
  return value
package/src/db/startup.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoundQuery, RecordId } from 'surrealdb'
2
2
  import { z } from 'zod'
3
3
 
4
- import type { SurrealDBService, SurrealDatabaseLogger } from '../db/service'
5
- import { TABLES } from '../db/tables'
6
- import { getErrorMessage } from '../utils/error'
4
+ import { getErrorMessage } from '../utils/errors'
5
+ import type { SurrealDBService, SurrealDatabaseLogger } from './service'
6
+ import { TABLES } from './tables'
7
7
 
8
8
  const DATABASE_BOOTSTRAP_KEY = 'database-schema-ready'
9
9
  const DEFAULT_RETRY_DELAY_MS = 1_000
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './runtime'
1
+ export * from './create-runtime'
2
2
  export * from './ai'
3
3
  export * from './bifrost'
4
4
  export * from './config'
@@ -12,6 +12,7 @@ import {
12
12
  attachWorkerEvents,
13
13
  createTracedWorkerProcessor,
14
14
  createWorkerShutdown,
15
+ DEFAULT_JOB_RETENTION,
15
16
  registerShutdownSignals,
16
17
  } from '../workers/worker-utils'
17
18
  import type { WorkerHandle } from '../workers/worker-utils'
@@ -29,12 +30,7 @@ function getContextCompactionQueue(): Queue<ContextCompactionJob> {
29
30
  if (!_contextCompactionQueue) {
30
31
  _contextCompactionQueue = new Queue<ContextCompactionJob>(CONTEXT_COMPACTION_QUEUE, {
31
32
  connection: getRedisConnectionForBullMQ(),
32
- defaultJobOptions: {
33
- removeOnComplete: 200,
34
- removeOnFail: 200,
35
- attempts: 2,
36
- backoff: { type: 'exponential', delay: 3_000 },
37
- },
33
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 3_000 } },
38
34
  })
39
35
  }
40
36
  return _contextCompactionQueue
@@ -51,11 +47,11 @@ async function processContextCompactionJob(job: Job<ContextCompactionJob>): Prom
51
47
 
52
48
  const { entityId, contextSize } = job.data
53
49
  const workstreamRef = ensureRecordId(entityId, TABLES.WORKSTREAM)
54
- await workstreamService.setCompacting(workstreamRef, true)
50
+ await workstreamService.markCompacting(workstreamRef)
55
51
  try {
56
52
  await contextCompactionService.compactWorkstreamHistory({ workstreamId: workstreamRef, contextSize })
57
53
  } finally {
58
- await workstreamService.setCompacting(workstreamRef, false)
54
+ await workstreamService.clearCompacting(workstreamRef)
59
55
  }
60
56
  }
61
57
 
@@ -4,7 +4,12 @@ import { Queue, Worker } from 'bullmq'
4
4
  import type IORedis from 'ioredis'
5
5
 
6
6
  import type { chatLogger } from '../config/logger'
7
- import { attachWorkerEvents, createWorkerShutdown, registerShutdownSignals } from '../workers/worker-utils'
7
+ import {
8
+ attachWorkerEvents,
9
+ createWorkerShutdown,
10
+ DEFAULT_JOB_RETENTION,
11
+ registerShutdownSignals,
12
+ } from '../workers/worker-utils'
8
13
  import type { WorkerHandle } from '../workers/worker-utils'
9
14
 
10
15
  export type DocumentSourceChannel = string
@@ -82,12 +87,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
82
87
 
83
88
  queue = new Queue<TJob, unknown, string>(queueName, {
84
89
  connection: params.getConnectionForBullMQ(),
85
- defaultJobOptions: {
86
- removeOnComplete: 200,
87
- removeOnFail: 200,
88
- attempts: 3,
89
- backoff: { type: 'exponential', delay: 1000 },
90
- },
90
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
91
91
  })
92
92
 
93
93
  return queue
@@ -5,6 +5,8 @@ import { getRedisConnectionForBullMQ } from '../redis'
5
5
  import {
6
6
  attachWorkerEvents,
7
7
  getWorkerPath,
8
+ LONG_JOB_LOCK_DURATION_MS,
9
+ LOW_JOB_RETENTION,
8
10
  createWorkerShutdown,
9
11
  registerShutdownSignals,
10
12
  } from '../workers/worker-utils'
@@ -15,18 +17,15 @@ export interface MemoryConsolidationJob {
15
17
  }
16
18
 
17
19
  const MEMORY_CONSOLIDATION_QUEUE = 'memory-consolidation'
20
+ const MEMORY_CONSOLIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000
21
+ const MEMORY_CONSOLIDATION_JOB_ID = 'memory-consolidation-recurring'
18
22
 
19
23
  let _memoryConsolidationQueue: Queue<MemoryConsolidationJob> | null = null
20
24
  function getMemoryConsolidationQueue(): Queue<MemoryConsolidationJob> {
21
25
  if (!_memoryConsolidationQueue) {
22
26
  _memoryConsolidationQueue = new Queue<MemoryConsolidationJob>(MEMORY_CONSOLIDATION_QUEUE, {
23
27
  connection: getRedisConnectionForBullMQ(),
24
- defaultJobOptions: {
25
- removeOnComplete: 50,
26
- removeOnFail: 50,
27
- attempts: 2,
28
- backoff: { type: 'exponential', delay: 5000 },
29
- },
28
+ defaultJobOptions: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
30
29
  })
31
30
  }
32
31
  return _memoryConsolidationQueue
@@ -42,7 +41,7 @@ export async function scheduleRecurringConsolidation() {
42
41
  await getMemoryConsolidationQueue().add(
43
42
  'consolidate',
44
43
  {},
45
- { repeat: { every: 24 * 60 * 60 * 1000 }, jobId: 'memory-consolidation-recurring' },
44
+ { repeat: { every: MEMORY_CONSOLIDATION_INTERVAL_MS }, jobId: MEMORY_CONSOLIDATION_JOB_ID },
46
45
  )
47
46
  }
48
47
 
@@ -51,7 +50,7 @@ export function startMemoryConsolidationWorker(options: { registerSignals?: bool
51
50
  const processorPath = getWorkerPath('memory-consolidation.worker.ts')
52
51
  const worker = new Worker(MEMORY_CONSOLIDATION_QUEUE, processorPath, {
53
52
  connection: getRedisConnectionForBullMQ(),
54
- lockDuration: 600_000,
53
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
55
54
  concurrency: 1,
56
55
  })
57
56
 
@@ -10,6 +10,7 @@ import {
10
10
  attachWorkerEvents,
11
11
  createTracedWorkerProcessor,
12
12
  createWorkerShutdown,
13
+ DEFAULT_JOB_RETENTION,
13
14
  registerShutdownSignals,
14
15
  } from '../workers/worker-utils'
15
16
  import type { WorkerHandle } from '../workers/worker-utils'
@@ -43,12 +44,7 @@ function getPostChatMemoryQueue(): Queue<PostChatMemoryExtractionJob> {
43
44
  if (!_postChatMemoryQueue) {
44
45
  _postChatMemoryQueue = new Queue<PostChatMemoryExtractionJob>(POST_CHAT_MEMORY_QUEUE, {
45
46
  connection: getRedisConnectionForBullMQ(),
46
- defaultJobOptions: {
47
- removeOnComplete: 200,
48
- removeOnFail: 200,
49
- attempts: 3,
50
- backoff: { type: 'exponential', delay: 2_000 },
51
- },
47
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
52
48
  })
53
49
  }
54
50
  return _postChatMemoryQueue
@@ -9,6 +9,7 @@ import {
9
9
  attachWorkerEvents,
10
10
  createTracedWorkerProcessor,
11
11
  createWorkerShutdown,
12
+ DEFAULT_JOB_RETENTION,
12
13
  registerShutdownSignals,
13
14
  } from '../workers/worker-utils'
14
15
  import type { WorkerHandle } from '../workers/worker-utils'
@@ -26,12 +27,7 @@ function getRecentActivityTitleRefinementQueue(): Queue<RecentActivityTitleRefin
26
27
  RECENT_ACTIVITY_TITLE_REFINEMENT_QUEUE,
27
28
  {
28
29
  connection: getRedisConnectionForBullMQ(),
29
- defaultJobOptions: {
30
- removeOnComplete: 200,
31
- removeOnFail: 200,
32
- attempts: 3,
33
- backoff: { type: 'exponential', delay: 2_000 },
34
- },
30
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
35
31
  },
36
32
  )
37
33
  }
@@ -4,7 +4,9 @@ import { serverLogger } from '../config/logger'
4
4
  import { getRedisConnectionForBullMQ } from '../redis'
5
5
  import {
6
6
  attachWorkerEvents,
7
+ DEFAULT_JOB_RETENTION,
7
8
  getWorkerPath,
9
+ LONG_JOB_LOCK_DURATION_MS,
8
10
  createWorkerShutdown,
9
11
  registerShutdownSignals,
10
12
  } from '../workers/worker-utils'
@@ -25,12 +27,7 @@ function getRegularChatMemoryDigestQueue(): Queue<RegularChatMemoryDigestJob> {
25
27
  if (!_regularChatMemoryDigestQueue) {
26
28
  _regularChatMemoryDigestQueue = new Queue<RegularChatMemoryDigestJob>(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, {
27
29
  connection: getRedisConnectionForBullMQ(),
28
- defaultJobOptions: {
29
- removeOnComplete: 200,
30
- removeOnFail: 200,
31
- attempts: 2,
32
- backoff: { type: 'exponential', delay: 5000 },
33
- },
30
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
34
31
  })
35
32
  }
36
33
  return _regularChatMemoryDigestQueue
@@ -54,7 +51,7 @@ export function startRegularChatMemoryDigestWorker(options: { registerSignals?:
54
51
  const worker = new Worker(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, processorPath, {
55
52
  connection: getRedisConnectionForBullMQ(),
56
53
  concurrency: 1,
57
- lockDuration: 600_000,
54
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
58
55
  })
59
56
 
60
57
  attachWorkerEvents(worker, 'Regular chat memory digest', serverLogger)
@@ -4,7 +4,9 @@ import { serverLogger } from '../config/logger'
4
4
  import { getRedisConnectionForBullMQ } from '../redis'
5
5
  import {
6
6
  attachWorkerEvents,
7
+ DEFAULT_JOB_RETENTION,
7
8
  getWorkerPath,
9
+ LONG_JOB_LOCK_DURATION_MS,
8
10
  createWorkerShutdown,
9
11
  registerShutdownSignals,
10
12
  } from '../workers/worker-utils'
@@ -22,12 +24,7 @@ function getSkillExtractionQueue(): Queue<SkillExtractionJob> {
22
24
  if (!_skillExtractionQueue) {
23
25
  _skillExtractionQueue = new Queue<SkillExtractionJob>(SKILL_EXTRACTION_QUEUE, {
24
26
  connection: getRedisConnectionForBullMQ(),
25
- defaultJobOptions: {
26
- removeOnComplete: 200,
27
- removeOnFail: 200,
28
- attempts: 2,
29
- backoff: { type: 'exponential', delay: 5000 },
30
- },
27
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
31
28
  })
32
29
  }
33
30
  return _skillExtractionQueue
@@ -43,7 +40,7 @@ export function startSkillExtractionWorker(options: { registerSignals?: boolean
43
40
  const worker = new Worker(SKILL_EXTRACTION_QUEUE, processorPath, {
44
41
  connection: getRedisConnectionForBullMQ(),
45
42
  concurrency: 1,
46
- lockDuration: 600_000,
43
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
47
44
  })
48
45
 
49
46
  attachWorkerEvents(worker, 'Skill extraction', serverLogger)
@@ -10,6 +10,7 @@ import {
10
10
  attachWorkerEvents,
11
11
  createTracedWorkerProcessor,
12
12
  createWorkerShutdown,
13
+ DEFAULT_JOB_RETENTION,
13
14
  registerShutdownSignals,
14
15
  } from '../workers/worker-utils'
15
16
  import type { WorkerHandle } from '../workers/worker-utils'
@@ -26,12 +27,7 @@ function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob
26
27
  if (!_workstreamTitleGenerationQueue) {
27
28
  _workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
28
29
  connection: getRedisConnectionForBullMQ(),
29
- defaultJobOptions: {
30
- removeOnComplete: 200,
31
- removeOnFail: 200,
32
- attempts: 2,
33
- backoff: { type: 'exponential', delay: 2_000 },
34
- },
30
+ defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 2_000 } },
35
31
  })
36
32
  }
37
33
  return _workstreamTitleGenerationQueue
@@ -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
 
@@ -8,6 +8,7 @@ export {
8
8
  type RedisConnectionAccessor,
9
9
  } from './connection-accessor'
10
10
  export { withOrgMemoryLock } from './org-memory-lock'
11
+ export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
11
12
  export { createWorkstreamResumableContext } from './stream-context'
12
13
 
13
14
  export { createRedisConnectionManager }
@@ -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) {