@lota-sdk/core 0.4.7 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -5,12 +5,18 @@ import {
5
5
  recordIdSchema,
6
6
  } from '@lota-sdk/shared'
7
7
  import type { QueueJobError, QueueJobStatus } from '@lota-sdk/shared'
8
- import { RecordId } from 'surrealdb'
8
+ import { Context, Duration, Effect, Layer, Schedule } from 'effect'
9
+ import type { RecordId } from 'surrealdb'
9
10
  import { z } from 'zod'
10
11
 
11
12
  import { recordIdToString } from '../db/record-id'
12
- import { databaseService } from '../db/service'
13
+ import type { SurrealDBService } from '../db/service'
13
14
  import { TABLES } from '../db/tables'
15
+ import { isRetriableTransactionConflict } from '../db/transaction-conflict'
16
+ import { BadRequestError, DatabaseError } from '../effect/errors'
17
+ import { DatabaseServiceTag } from '../effect/services'
18
+ import { createDeterministicRecordId } from '../utils/crypto'
19
+ import { nowDate, unsafeDateFrom } from '../utils/date-time'
14
20
  import { compactRecord, readRecord, readString, readStringField, stringifyUnknown, truncateText } from '../utils/string'
15
21
 
16
22
  const QueueJobRowSchema = z.object({
@@ -52,7 +58,6 @@ const QueueJobAttemptRowSchema = z.object({
52
58
 
53
59
  const PERSISTENCE_MAX_ATTEMPTS = 4
54
60
  const PERSISTENCE_RETRY_BASE_DELAY_MS = 25
55
- const PERSISTENCE_RETRY_JITTER_MS = 25
56
61
 
57
62
  export interface TrackedBullJobLike {
58
63
  queueName: string
@@ -64,11 +69,6 @@ export interface TrackedBullJobLike {
64
69
  timestamp?: number
65
70
  }
66
71
 
67
- function buildDeterministicRecordId(table: string, key: string): RecordId {
68
- const digest = new Bun.CryptoHasher('sha256').update(key).digest('hex')
69
- return new RecordId(table, digest)
70
- }
71
-
72
72
  function sanitizeQueueValue(value: unknown, depth = 0): unknown {
73
73
  if (value === null || value === undefined) return value
74
74
  if (typeof value === 'string') return truncateText(value, 20_000)
@@ -117,21 +117,27 @@ function toQueueJobError(error: unknown): QueueJobError {
117
117
  return QueueJobErrorSchema.parse({ message: truncateText(stringifyUnknown(error) ?? 'Unknown error', 5_000) })
118
118
  }
119
119
 
120
- function getBullmqJobId(job: TrackedBullJobLike): string {
120
+ function getBullmqJobId(job: TrackedBullJobLike): Effect.Effect<string, BadRequestError> {
121
121
  const id = job.id
122
- if (typeof id === 'string' && id.length > 0) return id
123
- if (typeof id === 'number') return String(id)
124
- throw new Error(`BullMQ job for queue "${job.queueName}" is missing an id.`)
122
+ if (typeof id === 'string' && id.length > 0) return Effect.succeed(id)
123
+ if (typeof id === 'number') return Effect.succeed(String(id))
124
+ return Effect.fail(new BadRequestError({ message: `BullMQ job for queue "${job.queueName}" is missing an id.` }))
125
125
  }
126
126
 
127
- function getQueueJobRecordId(job: TrackedBullJobLike): RecordId {
128
- return buildDeterministicRecordId(TABLES.QUEUE_JOB, `${job.queueName}:${getBullmqJobId(job)}`)
127
+ function getQueueJobRecordId(job: TrackedBullJobLike): Effect.Effect<RecordId, BadRequestError> {
128
+ return getBullmqJobId(job).pipe(
129
+ Effect.map((bullmqJobId) => createDeterministicRecordId(TABLES.QUEUE_JOB, `${job.queueName}:${bullmqJobId}`)),
130
+ )
129
131
  }
130
132
 
131
- function getQueueJobAttemptRecordId(job: TrackedBullJobLike, attemptNumber: number): RecordId {
132
- return buildDeterministicRecordId(
133
- TABLES.QUEUE_JOB_ATTEMPT,
134
- `${job.queueName}:${getBullmqJobId(job)}:${attemptNumber}`,
133
+ function getQueueJobAttemptRecordId(
134
+ job: TrackedBullJobLike,
135
+ attemptNumber: number,
136
+ ): Effect.Effect<RecordId, BadRequestError> {
137
+ return getBullmqJobId(job).pipe(
138
+ Effect.map((bullmqJobId) =>
139
+ createDeterministicRecordId(TABLES.QUEUE_JOB_ATTEMPT, `${job.queueName}:${bullmqJobId}:${attemptNumber}`),
140
+ ),
135
141
  )
136
142
  }
137
143
 
@@ -178,225 +184,285 @@ function getQueuedStatus(job: TrackedBullJobLike): QueueJobStatus {
178
184
  return typeof delay === 'number' && delay > 0 ? 'delayed' : 'waiting'
179
185
  }
180
186
 
181
- function isRetriablePersistenceConflict(error: unknown): boolean {
182
- if (!(error instanceof Error)) return false
183
-
184
- const message = error.message.toLowerCase()
185
- return (
186
- message.includes('transaction conflict') ||
187
- message.includes('transaction read conflict') ||
188
- message.includes('read or write conflict') ||
189
- message.includes('write conflict') ||
190
- message.includes('resource busy') ||
191
- message.includes('this transaction can be retried')
192
- )
193
- }
194
-
195
- class QueueJobService {
196
- private async withPersistenceRetry<T>(work: () => Promise<T>): Promise<T> {
197
- let lastError: unknown = null
198
-
199
- for (let attempt = 1; attempt <= PERSISTENCE_MAX_ATTEMPTS; attempt += 1) {
200
- try {
201
- return await work()
202
- } catch (error) {
203
- lastError = error
204
- const hasMoreAttempts = attempt < PERSISTENCE_MAX_ATTEMPTS
205
- if (!isRetriablePersistenceConflict(error) || !hasMoreAttempts) {
206
- throw error
207
- }
208
-
209
- const backoffMs =
210
- PERSISTENCE_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * PERSISTENCE_RETRY_JITTER_MS)
211
- await Bun.sleep(backoffMs)
187
+ function tryQueueJobPersistence<A>(
188
+ message: string,
189
+ thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
190
+ ): Effect.Effect<A, DatabaseError> {
191
+ return Effect.suspend(() => {
192
+ try {
193
+ const value = thunk()
194
+ if (Effect.isEffect(value)) {
195
+ return value.pipe(Effect.mapError((cause) => new DatabaseError({ message, cause })))
212
196
  }
213
- }
214
-
215
- throw lastError instanceof Error ? lastError : new Error('Queue job persistence retry exhausted')
216
- }
217
197
 
218
- getQueueJobId(queueName: string, bullmqJobId: string): string {
219
- return recordIdToString(
220
- buildDeterministicRecordId(TABLES.QUEUE_JOB, `${queueName}:${bullmqJobId}`),
221
- TABLES.QUEUE_JOB,
222
- )
223
- }
224
-
225
- async recordEnqueued(job: TrackedBullJobLike, context?: Record<string, unknown>): Promise<string> {
226
- await databaseService.connect()
227
- return this.withPersistenceRetry(async () => {
228
- const queueJobId = getQueueJobRecordId(job)
229
- const queuedAt = typeof job.timestamp === 'number' ? new Date(job.timestamp) : new Date()
230
- const mergedContext = compactRecord({ ...extractJobContext(job.data), ...context })
231
-
232
- await databaseService.upsert(
233
- TABLES.QUEUE_JOB,
234
- queueJobId,
235
- compactRecord({
236
- queueName: job.queueName,
237
- jobName: job.name,
238
- bullmqJobId: getBullmqJobId(job),
239
- status: getQueuedStatus(job),
240
- data: wrapFlexibleValue(job.data),
241
- options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
242
- context: Object.keys(mergedContext).length > 0 ? sanitizeQueueValue(mergedContext) : undefined,
243
- deduplicationId: readDeduplicationId(job),
244
- maxAttempts: readMaxAttempts(job),
245
- queuedAt,
246
- }),
247
- QueueJobRowSchema,
248
- )
249
-
250
- return recordIdToString(queueJobId, TABLES.QUEUE_JOB)
251
- })
252
- }
253
-
254
- async markAttemptStarted(job: TrackedBullJobLike): Promise<string> {
255
- await databaseService.connect()
256
- const attemptNumber = resolveAttemptNumber(job)
257
- const queueJobId = getQueueJobRecordId(job)
258
- const startedAt = new Date()
259
-
260
- return this.withPersistenceRetry(async () => {
261
- await databaseService.upsert(
262
- TABLES.QUEUE_JOB,
263
- queueJobId,
264
- compactRecord({
265
- queueName: job.queueName,
266
- jobName: job.name,
267
- bullmqJobId: getBullmqJobId(job),
268
- status: 'active',
269
- data: wrapFlexibleValue(job.data),
270
- options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
271
- context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
272
- deduplicationId: readDeduplicationId(job),
273
- maxAttempts: readMaxAttempts(job),
274
- attemptCount: attemptNumber,
275
- queuedAt: typeof job.timestamp === 'number' ? new Date(job.timestamp) : startedAt,
276
- startedAt,
277
- }),
278
- QueueJobRowSchema,
279
- )
280
-
281
- const attemptId = getQueueJobAttemptRecordId(job, attemptNumber)
282
- await databaseService.upsert(
283
- TABLES.QUEUE_JOB_ATTEMPT,
284
- attemptId,
285
- { queueJobId, attemptNumber, status: 'active', startedAt },
286
- QueueJobAttemptRowSchema,
287
- )
288
-
289
- return recordIdToString(queueJobId, TABLES.QUEUE_JOB)
290
- })
291
- }
292
-
293
- async markAttemptCompleted(job: TrackedBullJobLike, result: unknown): Promise<void> {
294
- await databaseService.connect()
295
- const attemptNumber = resolveAttemptNumber(job)
296
- const queueJobId = getQueueJobRecordId(job)
297
- const attemptId = getQueueJobAttemptRecordId(job, attemptNumber)
298
- const completedAt = new Date()
299
-
300
- await this.withPersistenceRetry(async () => {
301
- const existingAttempt = await databaseService.findOne(
302
- TABLES.QUEUE_JOB_ATTEMPT,
303
- { id: attemptId },
304
- QueueJobAttemptRowSchema,
305
- )
306
-
307
- await databaseService.upsert(
308
- TABLES.QUEUE_JOB_ATTEMPT,
309
- attemptId,
310
- compactRecord({
311
- queueJobId,
312
- attemptNumber,
313
- status: 'completed',
314
- result: wrapFlexibleValue(result),
315
- startedAt: existingAttempt?.startedAt ?? completedAt,
316
- completedAt,
317
- durationMs: existingAttempt ? Math.max(0, completedAt.getTime() - existingAttempt.startedAt.getTime()) : 0,
318
- }),
319
- QueueJobAttemptRowSchema,
320
- )
321
-
322
- await databaseService.upsert(
323
- TABLES.QUEUE_JOB,
324
- queueJobId,
325
- compactRecord({
326
- queueName: job.queueName,
327
- jobName: job.name,
328
- bullmqJobId: getBullmqJobId(job),
329
- status: 'completed',
330
- data: wrapFlexibleValue(job.data),
331
- options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
332
- context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
333
- deduplicationId: readDeduplicationId(job),
334
- maxAttempts: readMaxAttempts(job),
335
- attemptCount: attemptNumber,
336
- queuedAt: typeof job.timestamp === 'number' ? new Date(job.timestamp) : completedAt,
337
- completedAt,
338
- result: wrapFlexibleValue(result),
339
- lastError: undefined,
340
- }),
341
- QueueJobRowSchema,
342
- )
343
- })
344
- }
198
+ return Effect.tryPromise({
199
+ try: () => Promise.resolve(value),
200
+ catch: (cause: unknown) => new DatabaseError({ message, cause }),
201
+ })
202
+ } catch (cause) {
203
+ return Effect.fail(new DatabaseError({ message, cause }))
204
+ }
205
+ })
206
+ }
345
207
 
346
- async markAttemptFailed(job: TrackedBullJobLike, error: unknown): Promise<void> {
347
- await databaseService.connect()
348
- const attemptNumber = resolveAttemptNumber(job)
349
- const queueJobId = getQueueJobRecordId(job)
350
- const attemptId = getQueueJobAttemptRecordId(job, attemptNumber)
351
- const failedAt = new Date()
352
- const normalizedError = toQueueJobError(error)
353
- const maxAttempts = readMaxAttempts(job)
354
- const terminal = typeof maxAttempts === 'number' ? attemptNumber >= maxAttempts : true
355
-
356
- await this.withPersistenceRetry(async () => {
357
- const existingAttempt = await databaseService.findOne(
358
- TABLES.QUEUE_JOB_ATTEMPT,
359
- { id: attemptId },
360
- QueueJobAttemptRowSchema,
361
- )
208
+ function withPersistenceRetry<T>(work: Effect.Effect<T, DatabaseError>): Effect.Effect<T, DatabaseError> {
209
+ return Effect.retry(work, {
210
+ times: PERSISTENCE_MAX_ATTEMPTS - 1,
211
+ schedule: Schedule.jittered(Schedule.exponential(Duration.millis(PERSISTENCE_RETRY_BASE_DELAY_MS), 2)),
212
+ while: isRetriableTransactionConflict,
213
+ })
214
+ }
362
215
 
363
- await databaseService.upsert(
364
- TABLES.QUEUE_JOB_ATTEMPT,
365
- attemptId,
366
- compactRecord({
367
- queueJobId,
368
- attemptNumber,
369
- status: 'failed',
370
- error: normalizedError,
371
- startedAt: existingAttempt?.startedAt ?? failedAt,
372
- completedAt: failedAt,
373
- durationMs: existingAttempt ? Math.max(0, failedAt.getTime() - existingAttempt.startedAt.getTime()) : 0,
374
- }),
375
- QueueJobAttemptRowSchema,
376
- )
216
+ function toPersistenceIdError(message: string, cause: unknown): DatabaseError {
217
+ return new DatabaseError({ message, cause })
218
+ }
377
219
 
378
- await databaseService.upsert(
220
+ export function makeQueueJobService(db: SurrealDBService) {
221
+ return {
222
+ getQueueJobId(queueName: string, bullmqJobId: string): string {
223
+ return recordIdToString(
224
+ createDeterministicRecordId(TABLES.QUEUE_JOB, `${queueName}:${bullmqJobId}`),
379
225
  TABLES.QUEUE_JOB,
380
- queueJobId,
381
- compactRecord({
382
- queueName: job.queueName,
383
- jobName: job.name,
384
- bullmqJobId: getBullmqJobId(job),
385
- status: terminal ? 'failed' : 'waiting',
386
- data: wrapFlexibleValue(job.data),
387
- options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
388
- context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
389
- deduplicationId: readDeduplicationId(job),
390
- maxAttempts,
391
- attemptCount: attemptNumber,
392
- queuedAt: typeof job.timestamp === 'number' ? new Date(job.timestamp) : failedAt,
393
- failedAt: terminal ? failedAt : undefined,
394
- lastError: normalizedError,
395
- }),
396
- QueueJobRowSchema,
397
226
  )
398
- })
227
+ },
228
+
229
+ recordEnqueued(job: TrackedBullJobLike, context?: Record<string, unknown>) {
230
+ return Effect.gen(function* () {
231
+ yield* tryQueueJobPersistence('Failed to connect before recording queued job metadata', () => db.connect())
232
+
233
+ const bullmqJobId = yield* getBullmqJobId(job)
234
+ const queueJobId = yield* getQueueJobRecordId(job)
235
+ const queuedAt = typeof job.timestamp === 'number' ? unsafeDateFrom(job.timestamp) : nowDate()
236
+ const mergedContext = compactRecord({ ...extractJobContext(job.data), ...context })
237
+
238
+ return yield* withPersistenceRetry(
239
+ Effect.gen(function* () {
240
+ yield* tryQueueJobPersistence('Failed to upsert queued job metadata', () =>
241
+ db.upsert(
242
+ TABLES.QUEUE_JOB,
243
+ queueJobId,
244
+ compactRecord({
245
+ queueName: job.queueName,
246
+ jobName: job.name,
247
+ bullmqJobId,
248
+ status: getQueuedStatus(job),
249
+ data: wrapFlexibleValue(job.data),
250
+ options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
251
+ context: Object.keys(mergedContext).length > 0 ? sanitizeQueueValue(mergedContext) : undefined,
252
+ deduplicationId: readDeduplicationId(job),
253
+ maxAttempts: readMaxAttempts(job),
254
+ queuedAt,
255
+ }),
256
+ QueueJobRowSchema,
257
+ ),
258
+ )
259
+
260
+ return recordIdToString(queueJobId, TABLES.QUEUE_JOB)
261
+ }),
262
+ )
263
+ })
264
+ },
265
+
266
+ markAttemptStarted(job: TrackedBullJobLike) {
267
+ return Effect.gen(function* () {
268
+ yield* tryQueueJobPersistence('Failed to connect before marking queue job attempt started', () => db.connect())
269
+
270
+ const attemptNumber = resolveAttemptNumber(job)
271
+ const bullmqJobId = yield* getBullmqJobId(job).pipe(
272
+ Effect.mapError((cause) => toPersistenceIdError('Failed to read BullMQ job id.', cause)),
273
+ )
274
+ const queueJobId = yield* getQueueJobRecordId(job).pipe(
275
+ Effect.mapError((cause) => toPersistenceIdError('Failed to derive queue job record id.', cause)),
276
+ )
277
+ const attemptId = yield* getQueueJobAttemptRecordId(job, attemptNumber).pipe(
278
+ Effect.mapError((cause) => toPersistenceIdError('Failed to derive queue job attempt record id.', cause)),
279
+ )
280
+ const startedAt = nowDate()
281
+ const queuedAt = typeof job.timestamp === 'number' ? unsafeDateFrom(job.timestamp) : startedAt
282
+
283
+ return yield* withPersistenceRetry(
284
+ Effect.gen(function* () {
285
+ yield* tryQueueJobPersistence('Failed to upsert queue job start metadata', () =>
286
+ db.upsert(
287
+ TABLES.QUEUE_JOB,
288
+ queueJobId,
289
+ compactRecord({
290
+ queueName: job.queueName,
291
+ jobName: job.name,
292
+ bullmqJobId,
293
+ status: 'active',
294
+ data: wrapFlexibleValue(job.data),
295
+ options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
296
+ context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
297
+ deduplicationId: readDeduplicationId(job),
298
+ maxAttempts: readMaxAttempts(job),
299
+ attemptCount: attemptNumber,
300
+ queuedAt,
301
+ startedAt,
302
+ }),
303
+ QueueJobRowSchema,
304
+ ),
305
+ )
306
+
307
+ yield* tryQueueJobPersistence('Failed to upsert queue job attempt start metadata', () =>
308
+ db.upsert(
309
+ TABLES.QUEUE_JOB_ATTEMPT,
310
+ attemptId,
311
+ { queueJobId, attemptNumber, status: 'active', startedAt },
312
+ QueueJobAttemptRowSchema,
313
+ ),
314
+ )
315
+
316
+ return recordIdToString(queueJobId, TABLES.QUEUE_JOB)
317
+ }),
318
+ )
319
+ })
320
+ },
321
+
322
+ markAttemptCompleted(job: TrackedBullJobLike, result: unknown) {
323
+ return Effect.gen(function* () {
324
+ yield* tryQueueJobPersistence('Failed to connect before marking queue job attempt completed', () =>
325
+ db.connect(),
326
+ )
327
+
328
+ const attemptNumber = resolveAttemptNumber(job)
329
+ const bullmqJobId = yield* getBullmqJobId(job)
330
+ const queueJobId = yield* getQueueJobRecordId(job)
331
+ const attemptId = yield* getQueueJobAttemptRecordId(job, attemptNumber)
332
+ const completedAt = nowDate()
333
+ const queuedAt = typeof job.timestamp === 'number' ? unsafeDateFrom(job.timestamp) : completedAt
334
+
335
+ yield* withPersistenceRetry(
336
+ Effect.gen(function* () {
337
+ const existingAttempt = yield* tryQueueJobPersistence(
338
+ 'Failed to load existing queue job attempt before completion update',
339
+ () => db.findOne(TABLES.QUEUE_JOB_ATTEMPT, { id: attemptId }, QueueJobAttemptRowSchema),
340
+ )
341
+
342
+ yield* tryQueueJobPersistence('Failed to upsert completed queue job attempt metadata', () =>
343
+ db.upsert(
344
+ TABLES.QUEUE_JOB_ATTEMPT,
345
+ attemptId,
346
+ compactRecord({
347
+ queueJobId,
348
+ attemptNumber,
349
+ status: 'completed',
350
+ result: wrapFlexibleValue(result),
351
+ startedAt: existingAttempt?.startedAt ?? completedAt,
352
+ completedAt,
353
+ durationMs: existingAttempt
354
+ ? Math.max(0, completedAt.getTime() - existingAttempt.startedAt.getTime())
355
+ : 0,
356
+ }),
357
+ QueueJobAttemptRowSchema,
358
+ ),
359
+ )
360
+
361
+ yield* tryQueueJobPersistence('Failed to upsert completed queue job metadata', () =>
362
+ db.upsert(
363
+ TABLES.QUEUE_JOB,
364
+ queueJobId,
365
+ compactRecord({
366
+ queueName: job.queueName,
367
+ jobName: job.name,
368
+ bullmqJobId,
369
+ status: 'completed',
370
+ data: wrapFlexibleValue(job.data),
371
+ options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
372
+ context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
373
+ deduplicationId: readDeduplicationId(job),
374
+ maxAttempts: readMaxAttempts(job),
375
+ attemptCount: attemptNumber,
376
+ queuedAt,
377
+ completedAt,
378
+ result: wrapFlexibleValue(result),
379
+ lastError: undefined,
380
+ }),
381
+ QueueJobRowSchema,
382
+ ),
383
+ )
384
+ }),
385
+ )
386
+ })
387
+ },
388
+
389
+ markAttemptFailed(job: TrackedBullJobLike, error: unknown) {
390
+ return Effect.gen(function* () {
391
+ yield* tryQueueJobPersistence('Failed to connect before marking queue job attempt failed', () => db.connect())
392
+
393
+ const attemptNumber = resolveAttemptNumber(job)
394
+ const bullmqJobId = yield* getBullmqJobId(job)
395
+ const queueJobId = yield* getQueueJobRecordId(job)
396
+ const attemptId = yield* getQueueJobAttemptRecordId(job, attemptNumber)
397
+ const failedAt = nowDate()
398
+ const queuedAt = typeof job.timestamp === 'number' ? unsafeDateFrom(job.timestamp) : failedAt
399
+ const normalizedError = toQueueJobError(error)
400
+ const maxAttempts = readMaxAttempts(job)
401
+ const terminal = typeof maxAttempts === 'number' ? attemptNumber >= maxAttempts : true
402
+
403
+ yield* withPersistenceRetry(
404
+ Effect.gen(function* () {
405
+ const existingAttempt = yield* tryQueueJobPersistence(
406
+ 'Failed to load existing queue job attempt before failure update',
407
+ () => db.findOne(TABLES.QUEUE_JOB_ATTEMPT, { id: attemptId }, QueueJobAttemptRowSchema),
408
+ )
409
+
410
+ yield* tryQueueJobPersistence('Failed to upsert failed queue job attempt metadata', () =>
411
+ db.upsert(
412
+ TABLES.QUEUE_JOB_ATTEMPT,
413
+ attemptId,
414
+ compactRecord({
415
+ queueJobId,
416
+ attemptNumber,
417
+ status: 'failed',
418
+ error: normalizedError,
419
+ startedAt: existingAttempt?.startedAt ?? failedAt,
420
+ completedAt: failedAt,
421
+ durationMs: existingAttempt
422
+ ? Math.max(0, failedAt.getTime() - existingAttempt.startedAt.getTime())
423
+ : 0,
424
+ }),
425
+ QueueJobAttemptRowSchema,
426
+ ),
427
+ )
428
+
429
+ yield* tryQueueJobPersistence('Failed to upsert failed queue job metadata', () =>
430
+ db.upsert(
431
+ TABLES.QUEUE_JOB,
432
+ queueJobId,
433
+ compactRecord({
434
+ queueName: job.queueName,
435
+ jobName: job.name,
436
+ bullmqJobId,
437
+ status: terminal ? 'failed' : 'waiting',
438
+ data: wrapFlexibleValue(job.data),
439
+ options: sanitizeQueueValue(job.opts) as Record<string, unknown>,
440
+ context: sanitizeQueueValue(extractJobContext(job.data)) as Record<string, unknown> | undefined,
441
+ deduplicationId: readDeduplicationId(job),
442
+ maxAttempts,
443
+ attemptCount: attemptNumber,
444
+ queuedAt,
445
+ failedAt: terminal ? failedAt : undefined,
446
+ lastError: normalizedError,
447
+ }),
448
+ QueueJobRowSchema,
449
+ ),
450
+ )
451
+ }),
452
+ )
453
+ })
454
+ },
399
455
  }
400
456
  }
401
457
 
402
- export const queueJobService = new QueueJobService()
458
+ export class QueueJobServiceTag extends Context.Service<QueueJobServiceTag, ReturnType<typeof makeQueueJobService>>()(
459
+ 'QueueJobService',
460
+ ) {}
461
+
462
+ export const QueueJobServiceLive = Layer.effect(
463
+ QueueJobServiceTag,
464
+ Effect.gen(function* () {
465
+ const db = yield* DatabaseServiceTag
466
+ return makeQueueJobService(db)
467
+ }),
468
+ )