@lota-sdk/core 0.4.9 → 0.4.10

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