@lota-sdk/core 0.4.9 → 0.4.11

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 (182) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +164 -82
  4. package/src/ai-gateway/index.ts +16 -1
  5. package/src/config/agent-defaults.ts +4 -107
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/background-processing.ts +1 -1
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +22 -25
  10. package/src/config/thread-defaults.ts +1 -10
  11. package/src/create-runtime.ts +145 -670
  12. package/src/db/base.service.ts +30 -38
  13. package/src/db/memory-query-builder.ts +2 -1
  14. package/src/db/memory-store.ts +29 -20
  15. package/src/db/memory.ts +188 -195
  16. package/src/db/service-normalization.ts +97 -64
  17. package/src/db/service.ts +496 -384
  18. package/src/db/startup.ts +30 -19
  19. package/src/effect/helpers.ts +30 -5
  20. package/src/effect/index.ts +7 -7
  21. package/src/effect/layers.ts +75 -72
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -71
  24. package/src/index.ts +13 -12
  25. package/src/queues/autonomous-job.queue.ts +177 -143
  26. package/src/queues/context-compaction.queue.ts +41 -39
  27. package/src/queues/delayed-node-promotion.queue.ts +61 -42
  28. package/src/queues/document-processor.queue.ts +5 -3
  29. package/src/queues/index.ts +1 -0
  30. package/src/queues/memory-consolidation.queue.ts +79 -53
  31. package/src/queues/organization-learning.queue.ts +70 -33
  32. package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
  33. package/src/queues/plan-scheduler.queue.ts +101 -97
  34. package/src/queues/post-chat-memory.queue.ts +56 -46
  35. package/src/queues/queue-factory.ts +146 -69
  36. package/src/queues/queues.service.ts +61 -0
  37. package/src/queues/title-generation.queue.ts +44 -44
  38. package/src/redis/connection.ts +181 -164
  39. package/src/redis/org-memory-lock.ts +24 -9
  40. package/src/redis/redis-lease-lock.ts +8 -1
  41. package/src/redis/stream-context.ts +17 -9
  42. package/src/runtime/agent-identity-overrides.ts +7 -3
  43. package/src/runtime/agent-runtime-policy.ts +10 -5
  44. package/src/runtime/agent-stream-helpers.ts +24 -15
  45. package/src/runtime/chat-run-orchestration.ts +1 -1
  46. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  47. package/src/runtime/context-compaction/context-compaction.ts +131 -85
  48. package/src/runtime/domain-layer.ts +203 -0
  49. package/src/runtime/execution-plan-visibility.ts +5 -2
  50. package/src/runtime/graph-designer.ts +0 -14
  51. package/src/runtime/helper-model.ts +8 -4
  52. package/src/runtime/index.ts +1 -1
  53. package/src/runtime/indexed-repositories-policy.ts +2 -6
  54. package/src/runtime/memory/memory-block.ts +19 -9
  55. package/src/runtime/memory/memory-pipeline.ts +53 -66
  56. package/src/runtime/memory/memory-scope.ts +33 -29
  57. package/src/runtime/plugin-resolution.ts +58 -62
  58. package/src/runtime/post-turn-side-effects.ts +139 -161
  59. package/src/runtime/retrieval-adapters.ts +4 -4
  60. package/src/runtime/runtime-config.ts +3 -9
  61. package/src/runtime/runtime-extensions.ts +0 -43
  62. package/src/runtime/runtime-lifecycle.ts +124 -0
  63. package/src/runtime/runtime-services.ts +455 -0
  64. package/src/runtime/runtime-worker-registry.ts +113 -30
  65. package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
  66. package/src/runtime/social-chat/social-chat-history.ts +24 -13
  67. package/src/runtime/social-chat/social-chat.ts +420 -369
  68. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
  69. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  70. package/src/runtime/thread-chat-helpers.ts +18 -9
  71. package/src/runtime/thread-turn-context.ts +28 -74
  72. package/src/runtime/turn-lifecycle.ts +6 -14
  73. package/src/services/agent-activity.service.ts +169 -176
  74. package/src/services/agent-executor.service.ts +207 -196
  75. package/src/services/artifact.service.ts +10 -5
  76. package/src/services/attachment.service.ts +16 -48
  77. package/src/services/autonomous-job.service.ts +81 -87
  78. package/src/services/background-work.service.ts +54 -0
  79. package/src/services/chat-run-registry.service.ts +3 -1
  80. package/src/services/context-compaction.service.ts +8 -10
  81. package/src/services/document-chunk.service.ts +8 -17
  82. package/src/services/execution-plan/execution-plan-graph.ts +122 -109
  83. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  84. package/src/services/execution-plan/execution-plan.service.ts +68 -51
  85. package/src/services/feedback-loop.service.ts +1 -1
  86. package/src/services/global-orchestrator.service.ts +49 -15
  87. package/src/services/graph-full-routing.ts +49 -37
  88. package/src/services/index.ts +1 -0
  89. package/src/services/institutional-memory.service.ts +8 -17
  90. package/src/services/learned-skill.service.ts +38 -35
  91. package/src/services/memory/memory-conversation.ts +10 -5
  92. package/src/services/memory/memory-errors.ts +27 -0
  93. package/src/services/memory/memory-org-memory.ts +14 -3
  94. package/src/services/memory/memory-preseeded.ts +10 -4
  95. package/src/services/memory/memory-utils.ts +2 -1
  96. package/src/services/memory/memory.service.ts +37 -52
  97. package/src/services/memory/rerank.service.ts +3 -11
  98. package/src/services/monitoring-window.service.ts +1 -1
  99. package/src/services/mutating-approval.service.ts +1 -1
  100. package/src/services/node-workspace.service.ts +2 -2
  101. package/src/services/notification.service.ts +16 -4
  102. package/src/services/organization-member.service.ts +1 -1
  103. package/src/services/organization.service.ts +34 -51
  104. package/src/services/ownership-dispatcher.service.ts +148 -95
  105. package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
  106. package/src/services/plan/plan-agent-query.service.ts +13 -9
  107. package/src/services/plan/plan-approval.service.ts +52 -48
  108. package/src/services/plan/plan-artifact.service.ts +2 -2
  109. package/src/services/plan/plan-builder.service.ts +2 -2
  110. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  111. package/src/services/plan/plan-compiler.service.ts +1 -1
  112. package/src/services/plan/plan-completion-side-effects.ts +99 -113
  113. package/src/services/plan/plan-coordination.service.ts +1 -1
  114. package/src/services/plan/plan-cycle.service.ts +171 -202
  115. package/src/services/plan/plan-deadline.service.ts +304 -307
  116. package/src/services/plan/plan-event-delivery.service.ts +84 -72
  117. package/src/services/plan/plan-executor-context.ts +2 -0
  118. package/src/services/plan/plan-executor-graph.ts +375 -353
  119. package/src/services/plan/plan-executor-helpers.ts +60 -75
  120. package/src/services/plan/plan-executor.service.ts +494 -489
  121. package/src/services/plan/plan-run.service.ts +12 -19
  122. package/src/services/plan/plan-scheduler.service.ts +89 -82
  123. package/src/services/plan/plan-template.service.ts +1 -1
  124. package/src/services/plan/plan-transaction-events.ts +8 -5
  125. package/src/services/plan/plan-validator.service.ts +1 -1
  126. package/src/services/plan/plan-workspace.service.ts +17 -11
  127. package/src/services/plugin-executor.service.ts +26 -21
  128. package/src/services/quality-metrics.service.ts +1 -1
  129. package/src/services/queue-job.service.ts +8 -17
  130. package/src/services/recent-activity-title.service.ts +22 -10
  131. package/src/services/recent-activity.service.ts +1 -1
  132. package/src/services/skill-resolver.service.ts +1 -1
  133. package/src/services/social-chat-history.service.ts +37 -20
  134. package/src/services/system-executor.service.ts +25 -20
  135. package/src/services/thread/thread-bootstrap.ts +37 -19
  136. package/src/services/thread/thread-listing.ts +2 -1
  137. package/src/services/thread/thread-memory-block.ts +18 -5
  138. package/src/services/thread/thread-message.service.ts +30 -13
  139. package/src/services/thread/thread-title.service.ts +1 -1
  140. package/src/services/thread/thread-turn-execution.ts +87 -83
  141. package/src/services/thread/thread-turn-preparation.service.ts +65 -40
  142. package/src/services/thread/thread-turn-streaming.ts +32 -36
  143. package/src/services/thread/thread-turn.ts +43 -29
  144. package/src/services/thread/thread.service.ts +32 -8
  145. package/src/services/user.service.ts +1 -1
  146. package/src/services/write-intent-validator.service.ts +1 -1
  147. package/src/storage/attachment-storage.service.ts +7 -4
  148. package/src/storage/generated-document-storage.service.ts +1 -1
  149. package/src/system-agents/context-compaction.agent.ts +1 -1
  150. package/src/system-agents/helper-agent-options.ts +1 -1
  151. package/src/system-agents/memory-reranker.agent.ts +1 -1
  152. package/src/system-agents/memory.agent.ts +1 -1
  153. package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
  154. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  155. package/src/system-agents/skill-extractor.agent.ts +1 -1
  156. package/src/system-agents/skill-manager.agent.ts +1 -1
  157. package/src/system-agents/thread-router.agent.ts +23 -20
  158. package/src/system-agents/title-generator.agent.ts +1 -1
  159. package/src/tools/execution-plan.tool.ts +36 -20
  160. package/src/tools/fetch-webpage.tool.ts +30 -22
  161. package/src/tools/firecrawl-client.ts +1 -6
  162. package/src/tools/plan-approval.tool.ts +9 -1
  163. package/src/tools/remember-memory.tool.ts +3 -6
  164. package/src/tools/research-topic.tool.ts +12 -3
  165. package/src/tools/search-web.tool.ts +26 -18
  166. package/src/tools/search.tool.ts +4 -5
  167. package/src/tools/team-think.tool.ts +139 -121
  168. package/src/utils/async.ts +15 -6
  169. package/src/utils/errors.ts +27 -15
  170. package/src/workers/bootstrap.ts +34 -58
  171. package/src/workers/memory-consolidation.worker.ts +4 -1
  172. package/src/workers/organization-learning.worker.ts +16 -3
  173. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  174. package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
  175. package/src/workers/skill-extraction.runner.ts +13 -15
  176. package/src/workers/worker-utils.ts +14 -8
  177. package/src/config/search.ts +0 -3
  178. package/src/effect/awaitable-effect.ts +0 -87
  179. package/src/effect/runtime-ref.ts +0 -25
  180. package/src/effect/runtime.ts +0 -31
  181. package/src/redis/runtime-connection.ts +0 -10
  182. package/src/runtime/agent-types.ts +0 -1
@@ -17,6 +17,8 @@ import type { SurrealDBService } from '../../db/service'
17
17
  import { TABLES } from '../../db/tables'
18
18
  import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
19
19
  import { DatabaseServiceTag } from '../../effect/services'
20
+ import type { PlanSchedulerQueueRuntime } from '../../queues/plan-scheduler.queue'
21
+ import { LotaQueuesServiceTag } from '../../queues/queues.service'
20
22
  import { nowDate, unsafeDateFrom } from '../../utils/date-time'
21
23
  import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
22
24
  import { PlanEventDeliveryServiceTag } from './plan-event-delivery.service'
@@ -86,6 +88,7 @@ interface PlanDeadlineDeps {
86
88
  planExecutorService: ReturnType<typeof makePlanExecutorService>
87
89
  planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
88
90
  planRunService: ReturnType<typeof makePlanRunService>
91
+ planSchedulerQueue: PlanSchedulerQueueRuntime
89
92
  }
90
93
 
91
94
  class PlanDeadlineError extends Schema.TaggedErrorClass<PlanDeadlineError>()('PlanDeadlineError', {
@@ -94,12 +97,12 @@ class PlanDeadlineError extends Schema.TaggedErrorClass<PlanDeadlineError>()('Pl
94
97
  }) {}
95
98
 
96
99
  export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
97
- const { db, planEventDeliveryService, planExecutorService, planRunService } = deps
100
+ const { db, planEventDeliveryService, planExecutorService, planRunService, planSchedulerQueue } = deps
98
101
  const effectTryPromise = makeEffectTryPromiseWithMessage(
99
102
  (message, cause) => new PlanDeadlineError({ message, cause }),
100
103
  )
101
- const loadPlanSchedulerQueue = () =>
102
- effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
104
+ const loadPlanSchedulerQueue = (): Effect.Effect<PlanSchedulerQueueRuntime, PlanDeadlineError> =>
105
+ Effect.succeed(planSchedulerQueue)
103
106
 
104
107
  function loadNodeSpec(nodeRun: PlanNodeRunRecord): Effect.Effect<PlanNodeSpecRecord | null, PlanDeadlineError> {
105
108
  return db
@@ -111,50 +114,48 @@ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
111
114
  .pipe(Effect.mapError((cause) => new PlanDeadlineError({ message: 'Failed to load plan node spec.', cause })))
112
115
  }
113
116
 
114
- function collectDeadlineSweepEffect(now: Date) {
115
- return Effect.gen(function* () {
116
- const activeNodeRuns = yield* effectTryPromise(
117
- () =>
118
- db.queryMany(
119
- new BoundQuery(`SELECT * FROM ${TABLES.PLAN_NODE_RUN} WHERE status IN $statuses`, {
120
- statuses: ['running', 'awaiting-human'],
121
- }),
122
- PlanNodeRunSchema,
123
- ),
124
- 'Failed to load active plan node runs for deadline sweep.',
125
- )
126
- const results = yield* Effect.forEach(
127
- activeNodeRuns,
128
- (nodeRun) =>
129
- Effect.gen(function* () {
130
- const nodeSpec = yield* loadNodeSpec(nodeRun).pipe(
131
- Effect.mapError(
132
- (cause) =>
133
- new PlanDeadlineError({
134
- message: `Failed to load node spec for ${nodeRun.nodeId}.`,
135
- cause: cause.cause,
136
- }),
137
- ),
138
- )
139
- if (!nodeSpec?.deadline) {
140
- return null
141
- }
142
-
143
- const evaluation = evaluateDeadline({
144
- deadline: nodeSpec.deadline,
145
- nodeStartedAt: unsafeDateFrom(nodeRun.startedAt ?? nodeRun.createdAt),
146
- now,
147
- })
148
-
149
- return { nodeRun, nodeSpec, evaluation }
117
+ const collectDeadlineSweepEffect = Effect.fn('PlanDeadline.collectDeadlineSweep')(function* (now: Date) {
118
+ const activeNodeRuns = yield* effectTryPromise(
119
+ () =>
120
+ db.queryMany(
121
+ new BoundQuery(`SELECT * FROM ${TABLES.PLAN_NODE_RUN} WHERE status IN $statuses`, {
122
+ statuses: ['running', 'awaiting-human'],
150
123
  }),
151
- { concurrency: 10 },
152
- )
153
- return { entries: results.filter((entry): entry is NonNullable<typeof entry> => entry !== null) }
154
- })
155
- }
124
+ PlanNodeRunSchema,
125
+ ),
126
+ 'Failed to load active plan node runs for deadline sweep.',
127
+ ).pipe(Effect.withSpan('PlanDeadline.loadActiveNodeRuns'))
128
+ const results = yield* Effect.forEach(
129
+ activeNodeRuns,
130
+ (nodeRun) =>
131
+ Effect.gen(function* () {
132
+ const nodeSpec = yield* loadNodeSpec(nodeRun).pipe(
133
+ Effect.mapError(
134
+ (cause) =>
135
+ new PlanDeadlineError({
136
+ message: `Failed to load node spec for ${nodeRun.nodeId}.`,
137
+ cause: cause.cause,
138
+ }),
139
+ ),
140
+ )
141
+ if (!nodeSpec?.deadline) {
142
+ return null
143
+ }
156
144
 
157
- function emitDeadlineEventEffect(params: {
145
+ const evaluation = evaluateDeadline({
146
+ deadline: nodeSpec.deadline,
147
+ nodeStartedAt: unsafeDateFrom(nodeRun.startedAt ?? nodeRun.createdAt),
148
+ now,
149
+ })
150
+
151
+ return { nodeRun, nodeSpec, evaluation }
152
+ }),
153
+ { concurrency: 10 },
154
+ ).pipe(Effect.withSpan('PlanDeadline.evaluateNodeRuns'))
155
+ return { entries: results.filter((entry): entry is NonNullable<typeof entry> => entry !== null) }
156
+ })
157
+
158
+ const emitDeadlineEventEffect = Effect.fn('PlanDeadline.emitDeadlineEvent')(function* (params: {
158
159
  run: PlanRunRecord
159
160
  nodeRun: PlanNodeRunRecord
160
161
  eventType: Extract<PlanEventType, 'deadline-warning' | 'deadline-missed' | 'escalation-triggered'>
@@ -167,68 +168,69 @@ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
167
168
  ? params.detail.dedupeKey.trim()
168
169
  : null
169
170
 
170
- return Effect.gen(function* () {
171
- const existing =
172
- dedupeKey === null
173
- ? []
174
- : yield* effectTryPromise(
175
- () =>
176
- db.queryMany(
177
- new BoundQuery(
178
- `SELECT * FROM ${TABLES.PLAN_EVENT}
171
+ const existing =
172
+ dedupeKey === null
173
+ ? []
174
+ : yield* effectTryPromise(
175
+ () =>
176
+ db.queryMany(
177
+ new BoundQuery(
178
+ `SELECT * FROM ${TABLES.PLAN_EVENT}
179
179
  WHERE runId = $runId
180
180
  AND nodeId = $nodeId
181
181
  AND eventType = $eventType
182
182
  AND detail.dedupeKey = $dedupeKey
183
183
  LIMIT 1`,
184
- {
185
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
186
- nodeId: params.nodeRun.nodeId,
187
- eventType: params.eventType,
188
- dedupeKey,
189
- },
190
- ),
191
- PlanEventSchema,
184
+ {
185
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
186
+ nodeId: params.nodeRun.nodeId,
187
+ eventType: params.eventType,
188
+ dedupeKey,
189
+ },
192
190
  ),
193
- 'Failed to load existing deadline event.',
194
- )
195
- if (existing.length > 0) {
196
- return
197
- }
191
+ PlanEventSchema,
192
+ ),
193
+ 'Failed to load existing deadline event.',
194
+ )
195
+ if (existing.length > 0) {
196
+ return
197
+ }
198
198
 
199
- const spec = yield* planRunService
200
- .getPlanSpecById(params.run.planSpecId)
201
- .pipe(
202
- Effect.mapError(
203
- (cause) => new PlanDeadlineError({ message: 'Failed to load plan spec for deadline event.', cause }),
204
- ),
205
- )
206
- const event = yield* effectTryPromise(
207
- () =>
208
- db.create(
209
- TABLES.PLAN_EVENT,
210
- {
211
- id: new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()),
212
- planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
213
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
214
- nodeId: params.nodeRun.nodeId,
215
- eventType: params.eventType,
216
- message: params.message,
217
- emittedBy: params.emittedBy,
218
- detail: params.detail,
219
- },
220
- PlanEventSchema,
221
- ),
222
- 'Failed to create deadline event.',
199
+ const spec = yield* planRunService
200
+ .getPlanSpecById(params.run.planSpecId)
201
+ .pipe(
202
+ Effect.mapError(
203
+ (cause) => new PlanDeadlineError({ message: 'Failed to load plan spec for deadline event.', cause }),
204
+ ),
223
205
  )
224
- yield* effectTryPromise(
225
- () => planEventDeliveryService.dispatchEvent(PlanEventSchema.parse(event)),
226
- 'Failed to dispatch deadline event.',
206
+ const event = yield* effectTryPromise(
207
+ () =>
208
+ db.create(
209
+ TABLES.PLAN_EVENT,
210
+ {
211
+ id: new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()),
212
+ planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
213
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
214
+ nodeId: params.nodeRun.nodeId,
215
+ eventType: params.eventType,
216
+ message: params.message,
217
+ emittedBy: params.emittedBy,
218
+ detail: params.detail,
219
+ },
220
+ PlanEventSchema,
221
+ ),
222
+ 'Failed to create deadline event.',
223
+ )
224
+ yield* planEventDeliveryService
225
+ .dispatchEvent(PlanEventSchema.parse(event))
226
+ .pipe(
227
+ Effect.mapError(
228
+ (error) => new PlanDeadlineError({ message: 'Failed to dispatch deadline event.', cause: error }),
229
+ ),
227
230
  )
228
- })
229
- }
231
+ })
230
232
 
231
- function applyDeadlineMissActionEffect(params: {
233
+ const applyDeadlineMissActionEffect = Effect.fn('PlanDeadline.applyDeadlineMissAction')(function* (params: {
232
234
  runId: RecordIdInput
233
235
  nodeId: string
234
236
  threadId: string
@@ -237,105 +239,99 @@ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
237
239
  emittedBy: string
238
240
  }) {
239
241
  const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
240
- return Effect.gen(function* () {
241
- const [run, nodeRun] = yield* Effect.all([
242
- planRunService
243
- .getRunById(params.runId)
244
- .pipe(
245
- Effect.mapError(
246
- (cause) => new PlanDeadlineError({ message: 'Failed to load plan run for deadline action.', cause }),
247
- ),
242
+ const [run, nodeRun] = yield* Effect.all([
243
+ planRunService
244
+ .getRunById(params.runId)
245
+ .pipe(
246
+ Effect.mapError(
247
+ (cause) => new PlanDeadlineError({ message: 'Failed to load plan run for deadline action.', cause }),
248
248
  ),
249
- planRunService
250
- .getNodeRunByNodeId(params.runId, params.nodeId)
251
- .pipe(
252
- Effect.mapError(
253
- (cause) => new PlanDeadlineError({ message: 'Failed to load plan node run for deadline action.', cause }),
254
- ),
249
+ ),
250
+ planRunService
251
+ .getNodeRunByNodeId(params.runId, params.nodeId)
252
+ .pipe(
253
+ Effect.mapError(
254
+ (cause) => new PlanDeadlineError({ message: 'Failed to load plan node run for deadline action.', cause }),
255
255
  ),
256
- ])
257
- const blockNode = (message: string): Effect.Effect<void, PlanDeadlineError> =>
258
- effectTryPromise(
259
- () =>
260
- planExecutorService.blockNodeOnDispatchFailure({
261
- threadId: params.threadId,
262
- runId: runIdStr,
263
- nodeId: params.nodeId,
264
- emittedBy: params.emittedBy,
265
- message,
266
- failureClass: 'timeout_exceeded',
267
- }),
268
- `Failed to ${message.toLowerCase()}.`,
256
+ ),
257
+ ])
258
+ const blockNode = (message: string): Effect.Effect<void, PlanDeadlineError> =>
259
+ planExecutorService
260
+ .blockNodeOnDispatchFailure({
261
+ threadId: params.threadId,
262
+ runId: runIdStr,
263
+ nodeId: params.nodeId,
264
+ emittedBy: params.emittedBy,
265
+ message,
266
+ failureClass: 'timeout_exceeded',
267
+ })
268
+ .pipe(
269
+ Effect.asVoid,
270
+ Effect.mapError((cause) => new PlanDeadlineError({ message: `Failed to ${message.toLowerCase()}.`, cause })),
269
271
  )
270
272
 
271
- switch (params.action) {
272
- case 'notify':
273
- yield* emitDeadlineEventEffect({
274
- run,
275
- nodeRun,
276
- eventType: 'deadline-missed',
277
- emittedBy: params.emittedBy,
278
- message: `Node "${params.nodeId}" has missed its deadline.`,
279
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:notify` },
280
- })
281
- return
273
+ switch (params.action) {
274
+ case 'notify':
275
+ yield* emitDeadlineEventEffect({
276
+ run,
277
+ nodeRun,
278
+ eventType: 'deadline-missed',
279
+ emittedBy: params.emittedBy,
280
+ message: `Node "${params.nodeId}" has missed its deadline.`,
281
+ detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:notify` },
282
+ })
283
+ return
282
284
 
283
- case 'escalate':
284
- yield* emitDeadlineEventEffect({
285
- run,
286
- nodeRun,
287
- eventType: 'escalation-triggered',
288
- emittedBy: params.emittedBy,
289
- message: `Node "${params.nodeId}" has missed its deadline and requires escalation.`,
290
- detail: {
291
- title: 'Deadline escalation',
292
- dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:escalate`,
293
- missedDeadline: true,
294
- },
295
- })
296
- return
285
+ case 'escalate':
286
+ yield* emitDeadlineEventEffect({
287
+ run,
288
+ nodeRun,
289
+ eventType: 'escalation-triggered',
290
+ emittedBy: params.emittedBy,
291
+ message: `Node "${params.nodeId}" has missed its deadline and requires escalation.`,
292
+ detail: {
293
+ title: 'Deadline escalation',
294
+ dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:escalate`,
295
+ missedDeadline: true,
296
+ },
297
+ })
298
+ return
297
299
 
298
- case 'block':
299
- yield* emitDeadlineEventEffect({
300
- run,
301
- nodeRun,
302
- eventType: 'deadline-missed',
303
- emittedBy: params.emittedBy,
304
- message: `Node "${params.nodeId}" has missed its deadline.`,
305
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:block` },
306
- })
307
- yield* blockNode('Deadline missed — node blocked')
308
- return
300
+ case 'block':
301
+ yield* emitDeadlineEventEffect({
302
+ run,
303
+ nodeRun,
304
+ eventType: 'deadline-missed',
305
+ emittedBy: params.emittedBy,
306
+ message: `Node "${params.nodeId}" has missed its deadline.`,
307
+ detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:block` },
308
+ })
309
+ yield* blockNode('Deadline missed — node blocked')
310
+ return
309
311
 
310
- case 'fail':
311
- yield* emitDeadlineEventEffect({
312
- run,
313
- nodeRun,
314
- eventType: 'deadline-missed',
315
- emittedBy: params.emittedBy,
316
- message: `Node "${params.nodeId}" has missed its deadline.`,
317
- detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:fail` },
318
- })
319
- yield* blockNode('Deadline missed — node failed')
320
- return
321
- }
322
- })
323
- }
312
+ case 'fail':
313
+ yield* emitDeadlineEventEffect({
314
+ run,
315
+ nodeRun,
316
+ eventType: 'deadline-missed',
317
+ emittedBy: params.emittedBy,
318
+ message: `Node "${params.nodeId}" has missed its deadline.`,
319
+ detail: { title: 'Deadline missed', dedupeKey: `plan-deadline:${runIdStr}:${params.nodeId}:missed:fail` },
320
+ })
321
+ yield* blockNode('Deadline missed — node failed')
322
+ return
323
+ }
324
+ })
324
325
 
325
- function maybeEmitEscalationPolicyEventEffect(params: {
326
- run: PlanRunRecord
327
- nodeRun: PlanNodeRunRecord
328
- nodeSpec: PlanNodeSpecRecord
329
- now: Date
330
- }) {
331
- const escalation = params.nodeSpec.escalation
332
- if (!escalation) return Effect.void
326
+ const maybeEmitEscalationPolicyEventEffect = Effect.fn('PlanDeadline.maybeEmitEscalationPolicyEvent')(
327
+ function* (params: { run: PlanRunRecord; nodeRun: PlanNodeRunRecord; nodeSpec: PlanNodeSpecRecord; now: Date }) {
328
+ const escalation = params.nodeSpec.escalation
329
+ if (!escalation) return
333
330
 
334
- const startedAt = unsafeDateFrom(params.nodeRun.startedAt ?? params.nodeRun.createdAt)
335
- const runIdStr = recordIdToString(params.run.id, TABLES.PLAN_RUN)
336
- const baseKey = `plan-escalation:${runIdStr}:${params.nodeRun.nodeId}`
331
+ const startedAt = unsafeDateFrom(params.nodeRun.startedAt ?? params.nodeRun.createdAt)
332
+ const runIdStr = recordIdToString(params.run.id, TABLES.PLAN_RUN)
333
+ const baseKey = `plan-escalation:${runIdStr}:${params.nodeRun.nodeId}`
337
334
 
338
- return Effect.gen(function* () {
339
335
  if (escalation.autoEscalateAfterMinutes) {
340
336
  const thresholdMs = escalation.autoEscalateAfterMinutes * 60_000
341
337
  if (params.now.getTime() - startedAt.getTime() >= thresholdMs) {
@@ -378,146 +374,140 @@ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
378
374
  })
379
375
  }
380
376
  }
381
- })
382
- }
377
+ },
378
+ )
383
379
 
384
- const enqueueDeadlineCheckEffect = (scheduledFor: Date) =>
385
- Effect.gen(function* () {
386
- const { enqueueDeadlineCheck } = yield* loadPlanSchedulerQueue()
387
- yield* effectTryPromise(() => enqueueDeadlineCheck(scheduledFor), 'Failed to enqueue deadline check.')
388
- })
380
+ const enqueueDeadlineCheckEffect = Effect.fn('PlanDeadline.enqueueDeadlineCheck')(function* (scheduledFor: Date) {
381
+ const { enqueueDeadlineCheck } = yield* loadPlanSchedulerQueue()
382
+ yield* effectTryPromise(() => enqueueDeadlineCheck(scheduledFor), 'Failed to enqueue deadline check.')
383
+ })
389
384
 
390
- const checkDeadlinesEffect = (now?: Date) => {
385
+ const checkDeadlinesEffect = Effect.fn('PlanDeadline.checkDeadlines')(function* (now?: Date) {
391
386
  const currentTime = now ?? nowDate()
392
387
 
393
- return Effect.gen(function* () {
394
- const sweep = yield* collectDeadlineSweepEffect(currentTime)
395
- if (sweep.entries.length === 0) {
396
- return
397
- }
398
-
399
- const runCache = new Map<string, PlanRunRecord>()
388
+ const sweep = yield* collectDeadlineSweepEffect(currentTime)
389
+ if (sweep.entries.length === 0) {
390
+ return
391
+ }
400
392
 
401
- const handleEntry = (entry: (typeof sweep.entries)[number]): Effect.Effect<void, PlanDeadlineError> =>
402
- Effect.gen(function* () {
403
- const deadline = entry.nodeSpec.deadline
404
- if (!deadline) {
405
- return
406
- }
393
+ const runCache = new Map<string, PlanRunRecord>()
407
394
 
408
- const runIdStr = recordIdToString(entry.nodeRun.runId, TABLES.PLAN_RUN)
409
- const cachedRun = runCache.get(runIdStr)
410
- const run =
411
- cachedRun ??
412
- (yield* effectTryPromise(
413
- () =>
414
- db.findOne(
415
- TABLES.PLAN_RUN,
416
- { id: ensureRecordId(entry.nodeRun.runId, TABLES.PLAN_RUN) },
417
- PlanRunSchema,
418
- ),
419
- 'Failed to load plan run during deadline check.',
420
- ))
421
- if (!run) {
422
- return
423
- }
424
- runCache.set(runIdStr, run)
395
+ const handleEntry = (entry: (typeof sweep.entries)[number]): Effect.Effect<void, PlanDeadlineError> =>
396
+ Effect.gen(function* () {
397
+ const deadline = entry.nodeSpec.deadline
398
+ if (!deadline) {
399
+ return
400
+ }
425
401
 
426
- const dedupeKeyBase = `plan-deadline:${runIdStr}:${entry.nodeRun.nodeId}`
427
- const actionEffect =
428
- entry.evaluation.status === 'warning'
402
+ const runIdStr = recordIdToString(entry.nodeRun.runId, TABLES.PLAN_RUN)
403
+ const cachedRun = runCache.get(runIdStr)
404
+ const run =
405
+ cachedRun ??
406
+ (yield* effectTryPromise(
407
+ () =>
408
+ db.findOne(TABLES.PLAN_RUN, { id: ensureRecordId(entry.nodeRun.runId, TABLES.PLAN_RUN) }, PlanRunSchema),
409
+ 'Failed to load plan run during deadline check.',
410
+ ))
411
+ if (!run) {
412
+ return
413
+ }
414
+ runCache.set(runIdStr, run)
415
+
416
+ const dedupeKeyBase = `plan-deadline:${runIdStr}:${entry.nodeRun.nodeId}`
417
+ const actionEffect =
418
+ entry.evaluation.status === 'warning'
419
+ ? emitDeadlineEventEffect({
420
+ run,
421
+ nodeRun: entry.nodeRun,
422
+ eventType: 'deadline-warning',
423
+ emittedBy: 'plan-deadline-checker',
424
+ message:
425
+ entry.evaluation.activeReminder?.message ??
426
+ `Node "${entry.nodeRun.nodeId}" is approaching its deadline.`,
427
+ detail: {
428
+ title: 'Deadline approaching',
429
+ reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
430
+ dedupeKey: `${dedupeKeyBase}:warning:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
431
+ },
432
+ })
433
+ : entry.evaluation.status === 'escalated'
429
434
  ? emitDeadlineEventEffect({
430
435
  run,
431
436
  nodeRun: entry.nodeRun,
432
- eventType: 'deadline-warning',
437
+ eventType: 'escalation-triggered',
433
438
  emittedBy: 'plan-deadline-checker',
434
439
  message:
435
440
  entry.evaluation.activeReminder?.message ??
436
- `Node "${entry.nodeRun.nodeId}" is approaching its deadline.`,
441
+ `Node "${entry.nodeRun.nodeId}" deadline requires escalation.`,
437
442
  detail: {
438
- title: 'Deadline approaching',
443
+ title: 'Deadline escalation',
439
444
  reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
440
- dedupeKey: `${dedupeKeyBase}:warning:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
445
+ dedupeKey: `${dedupeKeyBase}:escalated:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
441
446
  },
442
447
  })
443
- : entry.evaluation.status === 'escalated'
444
- ? emitDeadlineEventEffect({
445
- run,
446
- nodeRun: entry.nodeRun,
447
- eventType: 'escalation-triggered',
448
- emittedBy: 'plan-deadline-checker',
449
- message:
450
- entry.evaluation.activeReminder?.message ??
451
- `Node "${entry.nodeRun.nodeId}" deadline requires escalation.`,
452
- detail: {
453
- title: 'Deadline escalation',
454
- reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
455
- dedupeKey: `${dedupeKeyBase}:escalated:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
456
- },
457
- })
458
- : applyDeadlineMissActionEffect({
459
- runId: entry.nodeRun.runId,
460
- nodeId: entry.nodeRun.nodeId,
461
- threadId: recordIdToString(run.threadId, TABLES.THREAD),
462
- organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
463
- action: deadline.missAction,
464
- emittedBy: 'plan-deadline-checker',
465
- })
466
-
467
- yield* actionEffect
468
- yield* maybeEmitEscalationPolicyEventEffect({
469
- run,
470
- nodeRun: entry.nodeRun,
471
- nodeSpec: entry.nodeSpec,
472
- now: currentTime,
473
- })
474
- })
475
-
476
- yield* Effect.forEach(
477
- sweep.entries.filter((entry) => entry.evaluation.status !== 'ok'),
478
- handleEntry,
479
- { concurrency: 3, discard: true },
480
- )
481
-
482
- const nextTriggerAt =
483
- sweep.entries
484
- .map((entry) => entry.evaluation.nextTriggerAt)
485
- .filter((value): value is Date => {
486
- if (!value) return false
487
- return value.getTime() > currentTime.getTime()
488
- })
489
- .sort((a, b) => a.getTime() - b.getTime())
490
- .at(0) ?? null
448
+ : applyDeadlineMissActionEffect({
449
+ runId: entry.nodeRun.runId,
450
+ nodeId: entry.nodeRun.nodeId,
451
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
452
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
453
+ action: deadline.missAction,
454
+ emittedBy: 'plan-deadline-checker',
455
+ })
491
456
 
492
- if (nextTriggerAt) {
493
- yield* enqueueDeadlineCheckEffect(nextTriggerAt)
494
- }
495
- })
496
- }
457
+ yield* actionEffect
458
+ yield* maybeEmitEscalationPolicyEventEffect({
459
+ run,
460
+ nodeRun: entry.nodeRun,
461
+ nodeSpec: entry.nodeSpec,
462
+ now: currentTime,
463
+ })
464
+ })
465
+
466
+ yield* Effect.forEach(
467
+ sweep.entries.filter((entry) => entry.evaluation.status !== 'ok'),
468
+ handleEntry,
469
+ { concurrency: 3, discard: true },
470
+ ).pipe(Effect.withSpan('PlanDeadline.processDeadlineEntries'))
471
+
472
+ const nextTriggerAt =
473
+ sweep.entries
474
+ .map((entry) => entry.evaluation.nextTriggerAt)
475
+ .filter((value): value is Date => {
476
+ if (!value) return false
477
+ return value.getTime() > currentTime.getTime()
478
+ })
479
+ .sort((a, b) => a.getTime() - b.getTime())
480
+ .at(0) ?? null
497
481
 
498
- const recoverDeadlineChecksEffect = (now = nowDate()) =>
499
- Effect.gen(function* () {
500
- const sweep = yield* collectDeadlineSweepEffect(now)
501
- const hasDueAction = sweep.entries.some((entry) => entry.evaluation.status !== 'ok')
502
- if (hasDueAction) {
503
- yield* enqueueDeadlineCheckEffect(now)
504
- return
505
- }
482
+ if (nextTriggerAt) {
483
+ yield* enqueueDeadlineCheckEffect(nextTriggerAt)
484
+ }
485
+ })
486
+
487
+ const recoverDeadlineChecksEffect = Effect.fn('PlanDeadline.recoverDeadlineChecks')(function* (
488
+ now: Date = nowDate(),
489
+ ) {
490
+ const sweep = yield* collectDeadlineSweepEffect(now)
491
+ const hasDueAction = sweep.entries.some((entry) => entry.evaluation.status !== 'ok')
492
+ if (hasDueAction) {
493
+ yield* enqueueDeadlineCheckEffect(now)
494
+ return
495
+ }
506
496
 
507
- const nextTriggerAt =
508
- sweep.entries
509
- .map((entry) => entry.evaluation.nextTriggerAt)
510
- .filter((value): value is Date => {
511
- if (!value) return false
512
- return value.getTime() > now.getTime()
513
- })
514
- .sort((a, b) => a.getTime() - b.getTime())
515
- .at(0) ?? null
497
+ const nextTriggerAt =
498
+ sweep.entries
499
+ .map((entry) => entry.evaluation.nextTriggerAt)
500
+ .filter((value): value is Date => {
501
+ if (!value) return false
502
+ return value.getTime() > now.getTime()
503
+ })
504
+ .sort((a, b) => a.getTime() - b.getTime())
505
+ .at(0) ?? null
516
506
 
517
- if (nextTriggerAt) {
518
- yield* enqueueDeadlineCheckEffect(nextTriggerAt)
519
- }
520
- })
507
+ if (nextTriggerAt) {
508
+ yield* enqueueDeadlineCheckEffect(nextTriggerAt)
509
+ }
510
+ })
521
511
 
522
512
  return {
523
513
  evaluateDeadline,
@@ -533,7 +523,7 @@ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
533
523
  export class PlanDeadlineServiceTag extends Context.Service<
534
524
  PlanDeadlineServiceTag,
535
525
  ReturnType<typeof makePlanDeadlineService>
536
- >()('PlanDeadlineService') {}
526
+ >()('@lota-sdk/core/PlanDeadlineService') {}
537
527
 
538
528
  export const PlanDeadlineServiceLive = Layer.effect(
539
529
  PlanDeadlineServiceTag,
@@ -542,6 +532,13 @@ export const PlanDeadlineServiceLive = Layer.effect(
542
532
  const planExecutorService = yield* PlanExecutorServiceTag
543
533
  const planEventDeliveryService = yield* PlanEventDeliveryServiceTag
544
534
  const planRunService = yield* PlanRunServiceTag
545
- return makePlanDeadlineService({ db, planExecutorService, planEventDeliveryService, planRunService })
535
+ const queues = yield* LotaQueuesServiceTag
536
+ return makePlanDeadlineService({
537
+ db,
538
+ planExecutorService,
539
+ planEventDeliveryService,
540
+ planRunService,
541
+ planSchedulerQueue: queues.planScheduler,
542
+ })
546
543
  }),
547
544
  )