@lota-sdk/core 0.4.13 → 0.4.14

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 (138) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/memory/memory-block.ts +3 -9
  37. package/src/runtime/memory/memory-scope.ts +3 -1
  38. package/src/runtime/plugin-resolution.ts +2 -1
  39. package/src/runtime/post-turn-side-effects.ts +6 -5
  40. package/src/runtime/retrieval-adapters.ts +8 -20
  41. package/src/runtime/runtime-config.ts +3 -9
  42. package/src/runtime/runtime-extensions.ts +2 -4
  43. package/src/runtime/runtime-lifecycle.ts +56 -16
  44. package/src/runtime/runtime-services.ts +180 -102
  45. package/src/runtime/runtime-worker-registry.ts +3 -1
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  47. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  48. package/src/runtime/social-chat/social-chat.ts +356 -223
  49. package/src/runtime/specialist-runner.ts +3 -1
  50. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  51. package/src/runtime/thread-turn-context.ts +142 -102
  52. package/src/runtime/turn-lifecycle.ts +15 -46
  53. package/src/services/agent-activity.service.ts +1 -1
  54. package/src/services/agent-executor.service.ts +107 -77
  55. package/src/services/autonomous-job.service.ts +354 -293
  56. package/src/services/background-work.service.ts +3 -3
  57. package/src/services/context-compaction.service.ts +7 -2
  58. package/src/services/document-chunk.service.ts +50 -32
  59. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  60. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  61. package/src/services/feedback-loop.service.ts +5 -4
  62. package/src/services/graph-full-routing.ts +37 -36
  63. package/src/services/institutional-memory.service.ts +28 -30
  64. package/src/services/learned-skill.service.ts +107 -72
  65. package/src/services/memory/memory-errors.ts +4 -23
  66. package/src/services/memory/memory-org-memory.ts +10 -5
  67. package/src/services/memory/memory-rerank.ts +18 -6
  68. package/src/services/memory/memory.service.ts +170 -111
  69. package/src/services/memory/rerank.service.ts +29 -20
  70. package/src/services/organization-member.service.ts +1 -1
  71. package/src/services/organization.service.ts +69 -75
  72. package/src/services/ownership-dispatcher.service.ts +40 -39
  73. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  74. package/src/services/plan/plan-agent-query.service.ts +39 -31
  75. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  76. package/src/services/plan/plan-coordination.service.ts +2 -1
  77. package/src/services/plan/plan-cycle.service.ts +6 -5
  78. package/src/services/plan/plan-deadline.service.ts +57 -54
  79. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  80. package/src/services/plan/plan-executor-graph.ts +18 -15
  81. package/src/services/plan/plan-executor.service.ts +235 -262
  82. package/src/services/plan/plan-run.service.ts +169 -93
  83. package/src/services/plan/plan-scheduler.service.ts +192 -202
  84. package/src/services/plan/plan-template.service.ts +1 -1
  85. package/src/services/plan/plan-transaction-events.ts +1 -1
  86. package/src/services/plan/plan-workspace.service.ts +23 -14
  87. package/src/services/plugin-executor.service.ts +5 -9
  88. package/src/services/queue-job.service.ts +117 -59
  89. package/src/services/recent-activity-title.service.ts +13 -12
  90. package/src/services/recent-activity.service.ts +6 -1
  91. package/src/services/social-chat-history.service.ts +29 -25
  92. package/src/services/system-executor.service.ts +5 -9
  93. package/src/services/thread/thread-active-run.ts +2 -2
  94. package/src/services/thread/thread-listing.ts +61 -57
  95. package/src/services/thread/thread-memory-block.ts +73 -48
  96. package/src/services/thread/thread-message.service.ts +76 -65
  97. package/src/services/thread/thread-record-store.ts +8 -8
  98. package/src/services/thread/thread-title.service.ts +10 -4
  99. package/src/services/thread/thread-turn-execution.ts +43 -45
  100. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  101. package/src/services/thread/thread-turn-streaming.ts +82 -85
  102. package/src/services/thread/thread-turn.ts +8 -8
  103. package/src/services/thread/thread.service.ts +135 -100
  104. package/src/services/user.service.ts +45 -48
  105. package/src/storage/attachment-parser.ts +6 -2
  106. package/src/storage/attachment-storage.service.ts +5 -6
  107. package/src/storage/generated-document-storage.service.ts +1 -1
  108. package/src/system-agents/context-compaction.agent.ts +10 -9
  109. package/src/system-agents/delegated-agent-factory.ts +30 -6
  110. package/src/system-agents/memory-reranker.agent.ts +10 -9
  111. package/src/system-agents/memory.agent.ts +10 -9
  112. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  113. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  114. package/src/system-agents/skill-extractor.agent.ts +13 -12
  115. package/src/system-agents/skill-manager.agent.ts +13 -12
  116. package/src/system-agents/thread-router.agent.ts +10 -5
  117. package/src/system-agents/title-generator.agent.ts +13 -12
  118. package/src/tools/fetch-webpage.tool.ts +13 -13
  119. package/src/tools/memory-block.tool.ts +3 -1
  120. package/src/tools/plan-approval.tool.ts +4 -2
  121. package/src/tools/read-file-parts.tool.ts +10 -4
  122. package/src/tools/remember-memory.tool.ts +3 -1
  123. package/src/tools/research-topic.tool.ts +9 -5
  124. package/src/tools/search-web.tool.ts +16 -16
  125. package/src/tools/search.tool.ts +20 -5
  126. package/src/tools/team-think.tool.ts +61 -38
  127. package/src/utils/async.ts +5 -5
  128. package/src/utils/errors.ts +19 -18
  129. package/src/utils/sse-keepalive.ts +28 -25
  130. package/src/workers/bootstrap.ts +75 -11
  131. package/src/workers/memory-consolidation.worker.ts +82 -91
  132. package/src/workers/organization-learning.worker.ts +14 -4
  133. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  134. package/src/workers/skill-extraction.runner.ts +97 -61
  135. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  136. package/src/workers/utils/thread-message-query.ts +24 -24
  137. package/src/workers/worker-utils.ts +23 -4
  138. package/src/effect/helpers.ts +0 -123
@@ -7,8 +7,7 @@ import type { RecordIdInput } from '../../db/record-id'
7
7
  import { ensureRecordId, recordIdToString } from '../../db/record-id'
8
8
  import type { SurrealDBService } from '../../db/service'
9
9
  import { TABLES } from '../../db/tables'
10
- import { NotFoundError } from '../../effect/errors'
11
- import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
10
+ import { ERROR_TAGS, NotFoundError } from '../../effect/errors'
12
11
  import { DatabaseServiceTag } from '../../effect/services'
13
12
  import type { PlanSchedulerQueueRuntime } from '../../queues/plan-scheduler.queue'
14
13
  import { LotaQueuesServiceTag } from '../../queues/queues.service'
@@ -20,79 +19,74 @@ interface PlanSchedulerRuntimeDeps {
20
19
  recoverDeadlineChecks(): Promise<void>
21
20
  }
22
21
 
23
- class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()('PlanSchedulerError', {
22
+ class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()(ERROR_TAGS.PlanSchedulerError, {
24
23
  message: Schema.String,
25
24
  cause: Schema.optional(Schema.Defect),
26
25
  }) {}
27
26
 
28
- const effectTryPromise = makeEffectTryPromiseWithMessage((message, cause) => new PlanSchedulerError({ message, cause }))
27
+ function toPlanSchedulerError(message: string, cause: unknown): PlanSchedulerError {
28
+ return new PlanSchedulerError({ message, cause })
29
+ }
29
30
 
30
- function requireScheduleField<A>(value: A | undefined, message: string): A {
31
+ function requireScheduleFieldEffect<A>(value: A | undefined, message: string): Effect.Effect<A, PlanSchedulerError> {
31
32
  if (value === undefined) {
32
- throw new PlanSchedulerError({ message })
33
+ return Effect.fail(new PlanSchedulerError({ message }))
33
34
  }
34
- return value
35
+ return Effect.succeed(value)
35
36
  }
36
37
 
37
- function computeNextCronDate(cronExpression: string, baseTime: Date): Date {
38
+ function computeNextCronDateEffect(cronExpression: string, baseTime: Date): Effect.Effect<Date, PlanSchedulerError> {
38
39
  const parsedCron = Cron.parse(cronExpression)
39
40
  if (!Result.isSuccess(parsedCron)) {
40
- throw new PlanSchedulerError({ message: `Invalid cron expression: "${cronExpression}".` })
41
- }
42
-
43
- try {
44
- return Cron.next(parsedCron.success, baseTime)
45
- } catch (cause) {
46
- throw new PlanSchedulerError({
47
- message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
48
- cause,
49
- })
41
+ return Effect.fail(new PlanSchedulerError({ message: `Invalid cron expression: "${cronExpression}".` }))
50
42
  }
43
+ return Effect.try({
44
+ try: () => Cron.next(parsedCron.success, baseTime),
45
+ catch: (cause) =>
46
+ new PlanSchedulerError({
47
+ message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
48
+ cause,
49
+ }),
50
+ })
51
51
  }
52
52
 
53
- // `computeNextFireAt` must stay purely synchronous no Clock, no service lookups, no async.
54
- // `computeNextFireAtEffect` wraps it with `Effect.try` so the same logic is composable from
55
- // Effect callers without an `Effect.runSync` round trip.
56
- function computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
53
+ // `computeNextFireAtEffect` is the canonical Effect-channel form. A sync
54
+ // wrapper `computeNextFireAt` (below, on the service) is exposed via
55
+ // `Effect.runSync` for backward-compat sync call sites; the effect has no
56
+ // layer requirements so running it synchronously is safe.
57
+ function computeNextFireAtEffect(
58
+ spec: PlanScheduleSpec,
59
+ baseTime: Date = nowDate(),
60
+ ): Effect.Effect<Date, PlanSchedulerError> {
57
61
  switch (spec.type) {
58
62
  case 'immediate':
59
- return baseTime
63
+ return Effect.succeed(baseTime)
60
64
 
61
65
  case 'absolute':
62
- return unsafeDateFrom(requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.'))
66
+ return requireScheduleFieldEffect(spec.at, 'Absolute schedules require an "at" timestamp.').pipe(
67
+ Effect.map((at) => unsafeDateFrom(at)),
68
+ )
63
69
 
64
- case 'relative': {
65
- const delayMs = requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
66
- return unsafeDateFrom(baseTime.getTime() + delayMs)
67
- }
70
+ case 'relative':
71
+ return requireScheduleFieldEffect(spec.delayMs, 'Relative schedules require "delayMs".').pipe(
72
+ Effect.map((delayMs) => unsafeDateFrom(baseTime.getTime() + delayMs)),
73
+ )
68
74
 
69
- case 'cron': {
70
- const cronExpression = requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
71
- return computeNextCronDate(cronExpression, baseTime)
72
- }
75
+ case 'cron':
76
+ return requireScheduleFieldEffect(spec.cron, 'Cron schedules require a "cron" expression.').pipe(
77
+ Effect.flatMap((cronExpression) => computeNextCronDateEffect(cronExpression, baseTime)),
78
+ )
73
79
 
74
- case 'monitoring': {
75
- const intervalMs = requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
76
- return unsafeDateFrom(baseTime.getTime() + intervalMs)
77
- }
80
+ case 'monitoring':
81
+ return requireScheduleFieldEffect(spec.intervalMs, 'Monitoring schedules require "intervalMs".').pipe(
82
+ Effect.map((intervalMs) => unsafeDateFrom(baseTime.getTime() + intervalMs)),
83
+ )
78
84
 
79
85
  default:
80
- throw new PlanSchedulerError({ message: 'Unsupported schedule type in schedule specification.' })
86
+ return Effect.fail(new PlanSchedulerError({ message: 'Unsupported schedule type in schedule specification.' }))
81
87
  }
82
88
  }
83
89
 
84
- const isPlanSchedulerError = Schema.is(PlanSchedulerError)
85
-
86
- function computeNextFireAtEffect(spec: PlanScheduleSpec, baseTime?: Date): Effect.Effect<Date, PlanSchedulerError> {
87
- return Effect.try({
88
- try: () => computeNextFireAt(spec, baseTime),
89
- catch: (cause) =>
90
- isPlanSchedulerError(cause)
91
- ? cause
92
- : new PlanSchedulerError({ message: 'Failed to compute the next fire time.', cause }),
93
- })
94
- }
95
-
96
90
  export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: PlanSchedulerQueueRuntime) {
97
91
  const loadPlanSchedulerQueue = (): Effect.Effect<PlanSchedulerQueueRuntime, PlanSchedulerError> =>
98
92
  Effect.succeed(schedulerQueue)
@@ -108,35 +102,33 @@ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: P
108
102
  Effect.gen(function* () {
109
103
  const nextFireAt = yield* computeNextFireAtEffect(params.scheduleSpec)
110
104
  const now = nowDate()
111
- const record = yield* effectTryPromise(
112
- () =>
113
- db.create(
114
- TABLES.PLAN_SCHEDULE,
115
- {
116
- organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
117
- threadId: ensureRecordId(params.threadId, TABLES.THREAD),
118
- planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
119
- runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
120
- nodeId: params.nodeId,
121
- scheduleSpec: params.scheduleSpec,
122
- status: 'active',
123
- fireCount: 0,
124
- nextFireAt: toDatabaseDateTime(nextFireAt),
125
- createdAt: now,
126
- },
127
- PlanScheduleRecordSchema,
128
- ),
129
- 'Failed to create plan schedule.',
130
- )
131
- const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
132
- yield* effectTryPromise(
133
- () =>
134
- enqueueScheduleFire(
105
+ const record = yield* db
106
+ .create(
107
+ TABLES.PLAN_SCHEDULE,
108
+ {
109
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
110
+ threadId: ensureRecordId(params.threadId, TABLES.THREAD),
111
+ planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
112
+ runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
113
+ nodeId: params.nodeId,
114
+ scheduleSpec: params.scheduleSpec,
115
+ status: 'active',
116
+ fireCount: 0,
117
+ nextFireAt: toDatabaseDateTime(nextFireAt),
118
+ createdAt: now,
119
+ },
120
+ PlanScheduleRecordSchema,
121
+ )
122
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to create plan schedule.', cause)))
123
+ const queue = yield* loadPlanSchedulerQueue()
124
+ yield* Effect.tryPromise({
125
+ try: () =>
126
+ queue.enqueueScheduleFire(
135
127
  recordIdToString(record.id, TABLES.PLAN_SCHEDULE),
136
128
  Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
137
129
  ),
138
- 'Failed to enqueue schedule fire job.',
139
- )
130
+ catch: (cause) => toPlanSchedulerError('Failed to enqueue schedule fire job.', cause),
131
+ })
140
132
  return record
141
133
  })
142
134
 
@@ -154,88 +146,84 @@ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: P
154
146
  const nextFireAt: Date | null = isActive ? yield* computeNextFireAtEffect(schedule.scheduleSpec, now) : null
155
147
  const newStatus: 'active' | 'completed' = isActive ? 'active' : 'completed'
156
148
 
157
- yield* effectTryPromise(
158
- () =>
159
- db.update(
160
- TABLES.PLAN_SCHEDULE,
161
- schedule.id,
162
- {
163
- fireCount: newFireCount,
164
- lastFiredAt: now,
165
- nextFireAt: toDatabaseDateTime(nextFireAt),
166
- status: newStatus,
167
- },
168
- PlanScheduleRecordSchema,
169
- ),
170
- 'Failed to update fired schedule.',
171
- )
149
+ yield* db
150
+ .update(
151
+ TABLES.PLAN_SCHEDULE,
152
+ schedule.id,
153
+ { fireCount: newFireCount, lastFiredAt: now, nextFireAt: toDatabaseDateTime(nextFireAt), status: newStatus },
154
+ PlanScheduleRecordSchema,
155
+ )
156
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to update fired schedule.', cause)))
172
157
 
173
158
  if (newStatus === 'active') {
174
159
  if (!nextFireAt) {
175
160
  return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
176
161
  }
177
- const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
178
- yield* effectTryPromise(
179
- () =>
180
- enqueueScheduleFire(
162
+ const queue = yield* loadPlanSchedulerQueue()
163
+ yield* Effect.tryPromise({
164
+ try: () =>
165
+ queue.enqueueScheduleFire(
181
166
  recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
182
167
  Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
183
168
  ),
184
- 'Failed to enqueue next schedule fire job.',
185
- )
169
+ catch: (cause) => toPlanSchedulerError('Failed to enqueue next schedule fire job.', cause),
170
+ })
186
171
  }
187
172
 
188
173
  const runId = schedule.runId
189
174
  const nodeId = schedule.nodeId
190
175
  const promotedNode = Boolean(runId && nodeId)
191
176
  if (runId && nodeId) {
192
- yield* effectTryPromise(
193
- () =>
177
+ yield* Effect.tryPromise({
178
+ try: () =>
194
179
  runtimeDeps.promoteDelayedNode({
195
180
  runId: recordIdToString(runId, TABLES.PLAN_RUN),
196
181
  nodeId,
197
182
  emittedBy: 'plan-scheduler',
198
183
  }),
199
- 'Failed to promote delayed plan node.',
200
- )
184
+ catch: (cause) => toPlanSchedulerError('Failed to promote delayed plan node.', cause),
185
+ })
201
186
  }
202
187
 
203
188
  const advancedCycle =
204
189
  schedule.planSpecId && !schedule.runId
205
- ? yield* effectTryPromise(
206
- () =>
207
- db.findOne(
208
- TABLES.PLAN_CYCLE,
209
- { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
210
- PlanCycleRecordSchema,
190
+ ? yield* db
191
+ .findOne(
192
+ TABLES.PLAN_CYCLE,
193
+ { scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
194
+ PlanCycleRecordSchema,
195
+ )
196
+ .pipe(
197
+ Effect.mapError((cause) => toPlanSchedulerError('Failed to load plan cycle for schedule.', cause)),
198
+ Effect.tap((cycle) =>
199
+ cycle
200
+ ? Effect.tryPromise({
201
+ try: () => runtimeDeps.advanceCycle(cycle.id),
202
+ catch: (cause) => toPlanSchedulerError('Failed to advance plan cycle.', cause),
203
+ })
204
+ : Effect.void,
211
205
  ),
212
- 'Failed to load plan cycle for schedule.',
213
- ).pipe(
214
- Effect.tap((cycle) =>
215
- cycle
216
- ? effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
217
- : Effect.void,
218
- ),
219
- Effect.map((cycle) => cycle !== null),
220
- )
206
+ Effect.map((cycle) => cycle !== null),
207
+ )
221
208
  : false
222
209
 
223
210
  if (promotedNode || advancedCycle) {
224
- yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
211
+ yield* Effect.tryPromise({
212
+ try: () => runtimeDeps.recoverDeadlineChecks(),
213
+ catch: (cause) => toPlanSchedulerError('Failed to recover deadline checks.', cause),
214
+ })
225
215
  }
226
216
  })
227
217
 
228
218
  const fireScheduleByIdEffect = (scheduleId: string, runtimeDeps: PlanSchedulerRuntimeDeps) =>
229
219
  Effect.gen(function* () {
230
- const schedule = yield* effectTryPromise(
231
- () =>
232
- db.findOne(
233
- TABLES.PLAN_SCHEDULE,
234
- { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
235
- PlanScheduleRecordSchema,
236
- ),
237
- 'Failed to load schedule by id.',
238
- )
220
+ const schedule = yield* db
221
+ .findOne(
222
+ TABLES.PLAN_SCHEDULE,
223
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
224
+ PlanScheduleRecordSchema,
225
+ )
226
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to load schedule by id.', cause)))
239
227
  if (!schedule || schedule.status !== 'active') return
240
228
 
241
229
  yield* fireScheduleEffect(schedule, runtimeDeps)
@@ -243,30 +231,33 @@ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: P
243
231
 
244
232
  const recoverActiveSchedulesEffect = () =>
245
233
  Effect.gen(function* () {
246
- const activeSchedules = yield* effectTryPromise(
247
- () =>
248
- db.queryMany(
249
- new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
250
- status: 'active',
251
- }),
252
- PlanScheduleRecordSchema,
253
- ),
254
- 'Failed to load active schedules.',
255
- )
256
- const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
234
+ const activeSchedules = yield* db
235
+ .queryMany(
236
+ new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
237
+ status: 'active',
238
+ }),
239
+ PlanScheduleRecordSchema,
240
+ )
241
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to load active schedules.', cause)))
242
+ const queue = yield* loadPlanSchedulerQueue()
257
243
  yield* Effect.forEach(
258
244
  activeSchedules.filter(
259
- (schedule): schedule is typeof schedule & { nextFireAt: Date } => schedule.nextFireAt !== undefined,
245
+ (schedule: PlanScheduleRecord): schedule is PlanScheduleRecord & { nextFireAt: Date } =>
246
+ schedule.nextFireAt !== undefined,
260
247
  ),
261
248
  (schedule) =>
262
- effectTryPromise(
263
- () =>
264
- enqueueScheduleFire(
249
+ Effect.tryPromise({
250
+ try: () =>
251
+ queue.enqueueScheduleFire(
265
252
  recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
266
253
  Math.max(0, unsafeDateFrom(schedule.nextFireAt).getTime() - nowEpochMillis()),
267
254
  ),
268
- `Failed to re-enqueue schedule ${recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)}.`,
269
- ),
255
+ catch: (cause) =>
256
+ toPlanSchedulerError(
257
+ `Failed to re-enqueue schedule ${recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)}.`,
258
+ cause,
259
+ ),
260
+ }),
270
261
  { concurrency: 5, discard: true },
271
262
  )
272
263
  })
@@ -274,50 +265,50 @@ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: P
274
265
  const cancelScheduleEffect = (scheduleId: RecordIdInput) => {
275
266
  const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
276
267
  return Effect.gen(function* () {
277
- const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
278
- yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
279
- yield* effectTryPromise(
280
- () =>
281
- db.update(
282
- TABLES.PLAN_SCHEDULE,
283
- ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
284
- { status: 'cancelled' },
285
- PlanScheduleRecordSchema,
286
- ),
287
- 'Failed to cancel schedule.',
288
- )
268
+ const queue = yield* loadPlanSchedulerQueue()
269
+ yield* Effect.tryPromise({
270
+ try: () => queue.removeScheduleFireJob(idStr),
271
+ catch: (cause) => toPlanSchedulerError('Failed to remove schedule fire job.', cause),
272
+ })
273
+ yield* db
274
+ .update(
275
+ TABLES.PLAN_SCHEDULE,
276
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
277
+ { status: 'cancelled' },
278
+ PlanScheduleRecordSchema,
279
+ )
280
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to cancel schedule.', cause)))
289
281
  })
290
282
  }
291
283
 
292
284
  const pauseScheduleEffect = (scheduleId: RecordIdInput) => {
293
285
  const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
294
286
  return Effect.gen(function* () {
295
- const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
296
- yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
297
- yield* effectTryPromise(
298
- () =>
299
- db.update(
300
- TABLES.PLAN_SCHEDULE,
301
- ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
302
- { status: 'paused' },
303
- PlanScheduleRecordSchema,
304
- ),
305
- 'Failed to pause schedule.',
306
- )
287
+ const queue = yield* loadPlanSchedulerQueue()
288
+ yield* Effect.tryPromise({
289
+ try: () => queue.removeScheduleFireJob(idStr),
290
+ catch: (cause) => toPlanSchedulerError('Failed to remove schedule fire job.', cause),
291
+ })
292
+ yield* db
293
+ .update(
294
+ TABLES.PLAN_SCHEDULE,
295
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
296
+ { status: 'paused' },
297
+ PlanScheduleRecordSchema,
298
+ )
299
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to pause schedule.', cause)))
307
300
  })
308
301
  }
309
302
 
310
303
  const resumeScheduleEffect = (scheduleId: RecordIdInput) =>
311
304
  Effect.gen(function* () {
312
- const schedule = yield* effectTryPromise(
313
- () =>
314
- db.findOne(
315
- TABLES.PLAN_SCHEDULE,
316
- { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
317
- PlanScheduleRecordSchema,
318
- ),
319
- 'Failed to load schedule for resume.',
320
- )
305
+ const schedule = yield* db
306
+ .findOne(
307
+ TABLES.PLAN_SCHEDULE,
308
+ { id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
309
+ PlanScheduleRecordSchema,
310
+ )
311
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to load schedule for resume.', cause)))
321
312
  if (!schedule) {
322
313
  return yield* new NotFoundError({
323
314
  resource: TABLES.PLAN_SCHEDULE,
@@ -327,43 +318,42 @@ export function makePlanSchedulerService(db: SurrealDBService, schedulerQueue: P
327
318
  }
328
319
 
329
320
  const nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec)
330
- yield* effectTryPromise(
331
- () =>
332
- db.update(
333
- TABLES.PLAN_SCHEDULE,
334
- ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
335
- { status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
336
- PlanScheduleRecordSchema,
337
- ),
338
- 'Failed to resume schedule.',
339
- )
340
- const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
341
- yield* effectTryPromise(
342
- () =>
343
- enqueueScheduleFire(
321
+ yield* db
322
+ .update(
323
+ TABLES.PLAN_SCHEDULE,
324
+ ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
325
+ { status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
326
+ PlanScheduleRecordSchema,
327
+ )
328
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to resume schedule.', cause)))
329
+ const queue = yield* loadPlanSchedulerQueue()
330
+ yield* Effect.tryPromise({
331
+ try: () =>
332
+ queue.enqueueScheduleFire(
344
333
  recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
345
334
  Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
346
335
  ),
347
- 'Failed to enqueue resumed schedule.',
348
- )
336
+ catch: (cause) => toPlanSchedulerError('Failed to enqueue resumed schedule.', cause),
337
+ })
349
338
  })
350
339
 
351
340
  const listSchedulesEffect = (threadId: RecordIdInput) =>
352
- effectTryPromise(
353
- () =>
354
- db.findMany(
355
- TABLES.PLAN_SCHEDULE,
356
- { threadId: ensureRecordId(threadId, TABLES.THREAD) },
357
- PlanScheduleRecordSchema,
358
- { orderBy: 'createdAt', orderDir: 'ASC' },
359
- ),
360
- 'Failed to list schedules.',
361
- )
341
+ db
342
+ .findMany(TABLES.PLAN_SCHEDULE, { threadId: ensureRecordId(threadId, TABLES.THREAD) }, PlanScheduleRecordSchema, {
343
+ orderBy: 'createdAt',
344
+ orderDir: 'ASC',
345
+ })
346
+ .pipe(Effect.mapError((cause) => toPlanSchedulerError('Failed to list schedules.', cause)))
362
347
 
363
348
  return {
364
349
  createSchedule: createScheduleEffect,
350
+ /**
351
+ * Sync wrapper for legacy callers. Throws `PlanSchedulerError` on
352
+ * invalid specs. The underlying `computeNextFireAtEffect` has no layer
353
+ * requirements so `Effect.runSync` is safe here.
354
+ */
365
355
  computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
366
- return computeNextFireAt(spec, baseTime)
356
+ return Effect.runSync(computeNextFireAtEffect(spec, baseTime))
367
357
  },
368
358
  /** Called by the BullMQ worker when a fire-schedule job executes. */
369
359
  fireScheduleById: fireScheduleByIdEffect,
@@ -50,7 +50,7 @@ type InstantiateTemplateParams = {
50
50
  }
51
51
 
52
52
  class PlanTemplateNotFoundError extends Schema.TaggedErrorClass<PlanTemplateNotFoundError>()(
53
- 'PlanTemplateNotFoundError',
53
+ '@lota-sdk/core/PlanTemplateNotFoundError',
54
54
  { templateId: Schema.String, message: Schema.String },
55
55
  ) {}
56
56
 
@@ -5,7 +5,7 @@ import type { DatabaseTransaction, SurrealDBService } from '../../db/service'
5
5
  import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
6
6
 
7
7
  class PlanTransactionEventsError extends Schema.TaggedErrorClass<PlanTransactionEventsError>()(
8
- 'PlanTransactionEventsError',
8
+ '@lota-sdk/core/PlanTransactionEventsError',
9
9
  { message: Schema.String, cause: Schema.optional(Schema.Defect) },
10
10
  ) {}
11
11
 
@@ -1,14 +1,11 @@
1
1
  import { Context, Effect, Layer, Schema } from 'effect'
2
2
 
3
3
  import { ServiceError } from '../../effect/errors'
4
- import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
5
4
  import { RedisServiceTag } from '../../effect/services'
6
5
  import { toValidationError } from '../../effect/zod'
7
6
  import type { RedisConnectionManager } from '../../redis/connection'
8
7
  import { nowEpochMillis } from '../../utils/date-time'
9
8
 
10
- const tryWorkspacePromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
11
-
12
9
  const PlanWorkspaceEntrySchema = Schema.Struct({
13
10
  value: Schema.Unknown,
14
11
  version: Schema.Number,
@@ -59,10 +56,10 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
59
56
  timestamp: nowEpochMillis(),
60
57
  }
61
58
  const serialized = yield* Schema.encodeEffect(PlanWorkspaceEntryJsonSchema)(entry)
62
- yield* tryWorkspacePromise(
63
- () => conn.hset(hashKey, fieldKey, serialized),
64
- 'Failed to write plan workspace entry.',
65
- )
59
+ yield* Effect.tryPromise({
60
+ try: () => conn.hset(hashKey, fieldKey, serialized),
61
+ catch: (cause) => new ServiceError({ message: 'Failed to write plan workspace entry.', cause }),
62
+ })
66
63
  })
67
64
 
68
65
  const snapshotReadEffect = (params: { runId: string; readerNodeId: string; snapshotSequence?: number }) =>
@@ -70,7 +67,10 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
70
67
  const conn = redis.getConnection()
71
68
  const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
72
69
  const nodePrefix = `${params.readerNodeId}:`
73
- const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace snapshot.')
70
+ const all = yield* Effect.tryPromise({
71
+ try: () => conn.hgetall(hashKey),
72
+ catch: (cause) => new ServiceError({ message: 'Failed to read plan workspace snapshot.', cause }),
73
+ })
74
74
  const result: Record<string, PlanWorkspaceEntry> = {}
75
75
  for (const [fieldKey, raw] of Object.entries(all)) {
76
76
  if (!fieldKey.startsWith(nodePrefix)) continue
@@ -89,11 +89,17 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
89
89
  const hashKey = `${PLAN_WORKSPACE_KEY_PREFIX}${params.runId}`
90
90
  const key = params.key
91
91
  if (key !== undefined) {
92
- const raw = yield* tryWorkspacePromise(() => conn.hget(hashKey, key), 'Failed to read plan workspace entry.')
92
+ const raw = yield* Effect.tryPromise({
93
+ try: () => conn.hget(hashKey, key),
94
+ catch: (cause) => new ServiceError({ message: 'Failed to read plan workspace entry.', cause }),
95
+ })
93
96
  if (!raw) return {}
94
97
  return { [key]: yield* parsePlanWorkspaceEntryEffect(key, raw) }
95
98
  }
96
- const all = yield* tryWorkspacePromise(() => conn.hgetall(hashKey), 'Failed to read plan workspace entries.')
99
+ const all = yield* Effect.tryPromise({
100
+ try: () => conn.hgetall(hashKey),
101
+ catch: (cause) => new ServiceError({ message: 'Failed to read plan workspace entries.', cause }),
102
+ })
97
103
  const result: Record<string, PlanWorkspaceEntry> = {}
98
104
  for (const [k, v] of Object.entries(all)) {
99
105
  result[k] = yield* parsePlanWorkspaceEntryEffect(k, v)
@@ -103,10 +109,13 @@ export function makePlanWorkspaceService(redis: RedisConnectionManager) {
103
109
 
104
110
  const cleanupEffect = (runId: string) =>
105
111
  Effect.asVoid(
106
- tryWorkspacePromise(() => {
107
- const conn = redis.getConnection()
108
- return conn.del(`${PLAN_WORKSPACE_KEY_PREFIX}${runId}`)
109
- }, 'Failed to clean up plan workspace.'),
112
+ Effect.tryPromise({
113
+ try: () => {
114
+ const conn = redis.getConnection()
115
+ return conn.del(`${PLAN_WORKSPACE_KEY_PREFIX}${runId}`)
116
+ },
117
+ catch: (cause) => new ServiceError({ message: 'Failed to clean up plan workspace.', cause }),
118
+ }),
110
119
  )
111
120
 
112
121
  return {