@lota-sdk/core 0.4.8 → 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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,533 @@
1
+ import type {
2
+ DeadlineAction,
3
+ DeadlineReminder,
4
+ DeadlineSpec,
5
+ PlanEventType,
6
+ PlanNodeRunRecord,
7
+ PlanNodeSpecRecord,
8
+ PlanRunRecord,
9
+ } from '@lota-sdk/shared'
10
+ import { PlanEventSchema, PlanNodeRunSchema, PlanNodeSpecRecordSchema, PlanRunSchema } from '@lota-sdk/shared'
11
+ import { Context, Schema, Effect, Layer } from 'effect'
12
+ import { BoundQuery, RecordId } from 'surrealdb'
13
+
14
+ import type { RecordIdInput } from '../../db/record-id'
15
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
16
+ import type { SurrealDBService } from '../../db/service'
17
+ import { TABLES } from '../../db/tables'
18
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
19
+ import { DatabaseServiceTag } from '../../effect/services'
20
+ import { nowDate, unsafeDateFrom } from '../../utils/date-time'
21
+ import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
22
+ import { PlanEventDeliveryServiceTag } from './plan-event-delivery.service'
23
+ import type { makePlanExecutorService } from './plan-executor.service'
24
+ import { PlanExecutorServiceTag } from './plan-executor.service'
25
+ import type { makePlanRunService } from './plan-run.service'
26
+ import { PlanRunServiceTag } from './plan-run.service'
27
+
28
+ export type DeadlineEvaluationStatus = 'ok' | 'warning' | 'escalated' | 'missed'
29
+
30
+ export interface DeadlineEvaluationResult {
31
+ status: DeadlineEvaluationStatus
32
+ activeReminder?: DeadlineReminder
33
+ nextTriggerAt?: Date | null
34
+ }
35
+
36
+ function resolveDeadlineTime(deadline: DeadlineSpec, nodeStartedAt: Date): Date | null {
37
+ return (
38
+ (deadline.dueAt ? unsafeDateFrom(deadline.dueAt) : null) ??
39
+ (deadline.durationMs !== undefined ? unsafeDateFrom(nodeStartedAt.getTime() + deadline.durationMs) : null)
40
+ )
41
+ }
42
+
43
+ /** Pure deadline evaluation — exported for use by other services without needing the full PlanDeadlineService. */
44
+ export function evaluateDeadline(params: {
45
+ deadline: DeadlineSpec
46
+ nodeStartedAt: Date
47
+ now?: Date
48
+ }): DeadlineEvaluationResult {
49
+ const now = params.now ?? nowDate()
50
+ const deadlineTime = resolveDeadlineTime(params.deadline, params.nodeStartedAt)
51
+
52
+ if (!deadlineTime) {
53
+ return { status: 'ok', nextTriggerAt: null }
54
+ }
55
+
56
+ const reminderEntries = [...params.deadline.reminders]
57
+ .map((reminder) => ({ reminder, triggerAt: unsafeDateFrom(deadlineTime.getTime() - reminder.beforeMs) }))
58
+ .sort((a, b) => a.triggerAt.getTime() - b.triggerAt.getTime())
59
+
60
+ let activeReminder: DeadlineReminder | undefined
61
+ let nextTriggerAt: Date | null = null
62
+
63
+ for (const entry of reminderEntries) {
64
+ if (now.getTime() < entry.triggerAt.getTime()) {
65
+ nextTriggerAt = entry.triggerAt
66
+ break
67
+ }
68
+
69
+ activeReminder = entry.reminder
70
+ }
71
+
72
+ if (now.getTime() >= deadlineTime.getTime()) {
73
+ return { status: 'missed', nextTriggerAt: null }
74
+ }
75
+
76
+ if (activeReminder) {
77
+ const status: DeadlineEvaluationStatus = activeReminder.action === 'escalate' ? 'escalated' : 'warning'
78
+ return { status, activeReminder, nextTriggerAt: nextTriggerAt ?? deadlineTime }
79
+ }
80
+
81
+ return { status: 'ok', nextTriggerAt: nextTriggerAt ?? deadlineTime }
82
+ }
83
+
84
+ interface PlanDeadlineDeps {
85
+ db: SurrealDBService
86
+ planExecutorService: ReturnType<typeof makePlanExecutorService>
87
+ planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
88
+ planRunService: ReturnType<typeof makePlanRunService>
89
+ }
90
+
91
+ class PlanDeadlineError extends Schema.TaggedErrorClass<PlanDeadlineError>()('PlanDeadlineError', {
92
+ message: Schema.String,
93
+ cause: Schema.optional(Schema.Defect),
94
+ }) {}
95
+
96
+ export function makePlanDeadlineService(deps: PlanDeadlineDeps) {
97
+ const { db, planEventDeliveryService, planExecutorService, planRunService } = deps
98
+ const effectTryPromise = makeEffectTryPromiseWithMessage(
99
+ (message, cause) => new PlanDeadlineError({ message, cause }),
100
+ )
101
+ const loadPlanSchedulerQueue = () =>
102
+ effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
103
+
104
+ function loadNodeSpec(nodeRun: PlanNodeRunRecord): Effect.Effect<PlanNodeSpecRecord | null, PlanDeadlineError> {
105
+ return db
106
+ .findOne(
107
+ TABLES.PLAN_NODE_SPEC,
108
+ { planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC), nodeId: nodeRun.nodeId },
109
+ PlanNodeSpecRecordSchema,
110
+ )
111
+ .pipe(Effect.mapError((cause) => new PlanDeadlineError({ message: 'Failed to load plan node spec.', cause })))
112
+ }
113
+
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'],
120
+ }),
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
+ })
154
+
155
+ const emitDeadlineEventEffect = Effect.fn('PlanDeadline.emitDeadlineEvent')(function* (params: {
156
+ run: PlanRunRecord
157
+ nodeRun: PlanNodeRunRecord
158
+ eventType: Extract<PlanEventType, 'deadline-warning' | 'deadline-missed' | 'escalation-triggered'>
159
+ emittedBy: string
160
+ message: string
161
+ detail: Record<string, unknown>
162
+ }) {
163
+ const dedupeKey =
164
+ typeof params.detail.dedupeKey === 'string' && params.detail.dedupeKey.trim().length > 0
165
+ ? params.detail.dedupeKey.trim()
166
+ : null
167
+
168
+ const existing =
169
+ dedupeKey === null
170
+ ? []
171
+ : yield* effectTryPromise(
172
+ () =>
173
+ db.queryMany(
174
+ new BoundQuery(
175
+ `SELECT * FROM ${TABLES.PLAN_EVENT}
176
+ WHERE runId = $runId
177
+ AND nodeId = $nodeId
178
+ AND eventType = $eventType
179
+ AND detail.dedupeKey = $dedupeKey
180
+ LIMIT 1`,
181
+ {
182
+ runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
183
+ nodeId: params.nodeRun.nodeId,
184
+ eventType: params.eventType,
185
+ dedupeKey,
186
+ },
187
+ ),
188
+ PlanEventSchema,
189
+ ),
190
+ 'Failed to load existing deadline event.',
191
+ )
192
+ if (existing.length > 0) {
193
+ return
194
+ }
195
+
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
+ ),
202
+ )
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
+ )
228
+ })
229
+
230
+ const applyDeadlineMissActionEffect = Effect.fn('PlanDeadline.applyDeadlineMissAction')(function* (params: {
231
+ runId: RecordIdInput
232
+ nodeId: string
233
+ threadId: string
234
+ organizationId: string
235
+ action: DeadlineAction
236
+ emittedBy: string
237
+ }) {
238
+ const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
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 }),
245
+ ),
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 }),
252
+ ),
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,
262
+ emittedBy: params.emittedBy,
263
+ message,
264
+ failureClass: 'timeout_exceeded',
265
+ }),
266
+ `Failed to ${message.toLowerCase()}.`,
267
+ )
268
+
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
280
+
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
295
+
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
307
+
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
+ })
321
+
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}`
330
+
331
+ if (escalation.autoEscalateAfterMinutes) {
332
+ const thresholdMs = escalation.autoEscalateAfterMinutes * 60_000
333
+ if (params.now.getTime() - startedAt.getTime() >= thresholdMs) {
334
+ yield* emitDeadlineEventEffect({
335
+ run: params.run,
336
+ nodeRun: params.nodeRun,
337
+ eventType: 'escalation-triggered',
338
+ emittedBy: 'plan-deadline-checker',
339
+ message: `Node "${params.nodeRun.nodeId}" exceeded its auto-escalation threshold.`,
340
+ detail: {
341
+ title: 'Execution escalation',
342
+ dedupeKey: `${baseKey}:auto:${escalation.autoEscalateAfterMinutes}`,
343
+ autoEscalateAfterMinutes: escalation.autoEscalateAfterMinutes,
344
+ ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
345
+ ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
346
+ },
347
+ })
348
+ }
349
+ }
350
+
351
+ if (escalation.deadlineThresholdMinutes && params.nodeSpec.deadline) {
352
+ const dueAt = resolveDeadlineTime(params.nodeSpec.deadline, startedAt)
353
+ if (!dueAt) return
354
+
355
+ const thresholdMs = escalation.deadlineThresholdMinutes * 60_000
356
+ if (dueAt.getTime() - params.now.getTime() <= thresholdMs && dueAt.getTime() > params.now.getTime()) {
357
+ yield* emitDeadlineEventEffect({
358
+ run: params.run,
359
+ nodeRun: params.nodeRun,
360
+ eventType: 'escalation-triggered',
361
+ emittedBy: 'plan-deadline-checker',
362
+ message: `Node "${params.nodeRun.nodeId}" is inside its escalation threshold.`,
363
+ detail: {
364
+ title: 'Deadline escalation threshold',
365
+ dedupeKey: `${baseKey}:threshold:${escalation.deadlineThresholdMinutes}`,
366
+ deadlineThresholdMinutes: escalation.deadlineThresholdMinutes,
367
+ ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
368
+ ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
369
+ },
370
+ })
371
+ }
372
+ }
373
+ },
374
+ )
375
+
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
+ })
380
+
381
+ const checkDeadlinesEffect = Effect.fn('PlanDeadline.checkDeadlines')(function* (now?: Date) {
382
+ const currentTime = now ?? nowDate()
383
+
384
+ const sweep = yield* collectDeadlineSweepEffect(currentTime)
385
+ if (sweep.entries.length === 0) {
386
+ return
387
+ }
388
+
389
+ const runCache = new Map<string, PlanRunRecord>()
390
+
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
+ }
397
+
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'
430
+ ? emitDeadlineEventEffect({
431
+ run,
432
+ nodeRun: entry.nodeRun,
433
+ eventType: 'escalation-triggered',
434
+ emittedBy: 'plan-deadline-checker',
435
+ message:
436
+ entry.evaluation.activeReminder?.message ??
437
+ `Node "${entry.nodeRun.nodeId}" deadline requires escalation.`,
438
+ detail: {
439
+ title: 'Deadline escalation',
440
+ reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
441
+ dedupeKey: `${dedupeKeyBase}:escalated:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
442
+ },
443
+ })
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
+ })
452
+
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
477
+
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
+ }
492
+
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
502
+
503
+ if (nextTriggerAt) {
504
+ yield* enqueueDeadlineCheckEffect(nextTriggerAt)
505
+ }
506
+ })
507
+
508
+ return {
509
+ evaluateDeadline,
510
+ checkDeadlines: checkDeadlinesEffect,
511
+ recoverDeadlineChecks: recoverDeadlineChecksEffect,
512
+ applyDeadlineMissAction: applyDeadlineMissActionEffect,
513
+ maybeEmitEscalationPolicyEvent: maybeEmitEscalationPolicyEventEffect,
514
+ emitDeadlineEvent: emitDeadlineEventEffect,
515
+ enqueueDeadlineCheck: enqueueDeadlineCheckEffect,
516
+ }
517
+ }
518
+
519
+ export class PlanDeadlineServiceTag extends Context.Service<
520
+ PlanDeadlineServiceTag,
521
+ ReturnType<typeof makePlanDeadlineService>
522
+ >()('@lota-sdk/core/PlanDeadlineService') {}
523
+
524
+ export const PlanDeadlineServiceLive = Layer.effect(
525
+ PlanDeadlineServiceTag,
526
+ Effect.gen(function* () {
527
+ const db = yield* DatabaseServiceTag
528
+ const planExecutorService = yield* PlanExecutorServiceTag
529
+ const planEventDeliveryService = yield* PlanEventDeliveryServiceTag
530
+ const planRunService = yield* PlanRunServiceTag
531
+ return makePlanDeadlineService({ db, planExecutorService, planEventDeliveryService, planRunService })
532
+ }),
533
+ )