@lota-sdk/core 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,547 @@
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
+ 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 }
150
+ }),
151
+ { concurrency: 10 },
152
+ )
153
+ return { entries: results.filter((entry): entry is NonNullable<typeof entry> => entry !== null) }
154
+ })
155
+ }
156
+
157
+ function emitDeadlineEventEffect(params: {
158
+ run: PlanRunRecord
159
+ nodeRun: PlanNodeRunRecord
160
+ eventType: Extract<PlanEventType, 'deadline-warning' | 'deadline-missed' | 'escalation-triggered'>
161
+ emittedBy: string
162
+ message: string
163
+ detail: Record<string, unknown>
164
+ }) {
165
+ const dedupeKey =
166
+ typeof params.detail.dedupeKey === 'string' && params.detail.dedupeKey.trim().length > 0
167
+ ? params.detail.dedupeKey.trim()
168
+ : null
169
+
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}
179
+ WHERE runId = $runId
180
+ AND nodeId = $nodeId
181
+ AND eventType = $eventType
182
+ AND detail.dedupeKey = $dedupeKey
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,
192
+ ),
193
+ 'Failed to load existing deadline event.',
194
+ )
195
+ if (existing.length > 0) {
196
+ return
197
+ }
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.',
223
+ )
224
+ yield* effectTryPromise(
225
+ () => planEventDeliveryService.dispatchEvent(PlanEventSchema.parse(event)),
226
+ 'Failed to dispatch deadline event.',
227
+ )
228
+ })
229
+ }
230
+
231
+ function applyDeadlineMissActionEffect(params: {
232
+ runId: RecordIdInput
233
+ nodeId: string
234
+ threadId: string
235
+ organizationId: string
236
+ action: DeadlineAction
237
+ emittedBy: string
238
+ }) {
239
+ 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
+ ),
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
+ ),
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()}.`,
269
+ )
270
+
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
282
+
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
297
+
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
309
+
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
+ }
324
+
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
333
+
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}`
337
+
338
+ return Effect.gen(function* () {
339
+ if (escalation.autoEscalateAfterMinutes) {
340
+ const thresholdMs = escalation.autoEscalateAfterMinutes * 60_000
341
+ if (params.now.getTime() - startedAt.getTime() >= thresholdMs) {
342
+ yield* emitDeadlineEventEffect({
343
+ run: params.run,
344
+ nodeRun: params.nodeRun,
345
+ eventType: 'escalation-triggered',
346
+ emittedBy: 'plan-deadline-checker',
347
+ message: `Node "${params.nodeRun.nodeId}" exceeded its auto-escalation threshold.`,
348
+ detail: {
349
+ title: 'Execution escalation',
350
+ dedupeKey: `${baseKey}:auto:${escalation.autoEscalateAfterMinutes}`,
351
+ autoEscalateAfterMinutes: escalation.autoEscalateAfterMinutes,
352
+ ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
353
+ ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
354
+ },
355
+ })
356
+ }
357
+ }
358
+
359
+ if (escalation.deadlineThresholdMinutes && params.nodeSpec.deadline) {
360
+ const dueAt = resolveDeadlineTime(params.nodeSpec.deadline, startedAt)
361
+ if (!dueAt) return
362
+
363
+ const thresholdMs = escalation.deadlineThresholdMinutes * 60_000
364
+ if (dueAt.getTime() - params.now.getTime() <= thresholdMs && dueAt.getTime() > params.now.getTime()) {
365
+ yield* emitDeadlineEventEffect({
366
+ run: params.run,
367
+ nodeRun: params.nodeRun,
368
+ eventType: 'escalation-triggered',
369
+ emittedBy: 'plan-deadline-checker',
370
+ message: `Node "${params.nodeRun.nodeId}" is inside its escalation threshold.`,
371
+ detail: {
372
+ title: 'Deadline escalation threshold',
373
+ dedupeKey: `${baseKey}:threshold:${escalation.deadlineThresholdMinutes}`,
374
+ deadlineThresholdMinutes: escalation.deadlineThresholdMinutes,
375
+ ...(escalation.escalateToAgent ? { escalateToAgent: escalation.escalateToAgent } : {}),
376
+ ...(escalation.escalateToUser ? { escalateToUser: escalation.escalateToUser } : {}),
377
+ },
378
+ })
379
+ }
380
+ }
381
+ })
382
+ }
383
+
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
+ })
389
+
390
+ const checkDeadlinesEffect = (now?: Date) => {
391
+ const currentTime = now ?? nowDate()
392
+
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>()
400
+
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
+ }
407
+
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)
425
+
426
+ const dedupeKeyBase = `plan-deadline:${runIdStr}:${entry.nodeRun.nodeId}`
427
+ const actionEffect =
428
+ entry.evaluation.status === 'warning'
429
+ ? emitDeadlineEventEffect({
430
+ run,
431
+ nodeRun: entry.nodeRun,
432
+ eventType: 'deadline-warning',
433
+ emittedBy: 'plan-deadline-checker',
434
+ message:
435
+ entry.evaluation.activeReminder?.message ??
436
+ `Node "${entry.nodeRun.nodeId}" is approaching its deadline.`,
437
+ detail: {
438
+ title: 'Deadline approaching',
439
+ reminderBeforeMs: entry.evaluation.activeReminder?.beforeMs ?? null,
440
+ dedupeKey: `${dedupeKeyBase}:warning:${entry.evaluation.activeReminder?.beforeMs ?? 'default'}`,
441
+ },
442
+ })
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
491
+
492
+ if (nextTriggerAt) {
493
+ yield* enqueueDeadlineCheckEffect(nextTriggerAt)
494
+ }
495
+ })
496
+ }
497
+
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
+ }
506
+
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
516
+
517
+ if (nextTriggerAt) {
518
+ yield* enqueueDeadlineCheckEffect(nextTriggerAt)
519
+ }
520
+ })
521
+
522
+ return {
523
+ evaluateDeadline,
524
+ checkDeadlines: checkDeadlinesEffect,
525
+ recoverDeadlineChecks: recoverDeadlineChecksEffect,
526
+ applyDeadlineMissAction: applyDeadlineMissActionEffect,
527
+ maybeEmitEscalationPolicyEvent: maybeEmitEscalationPolicyEventEffect,
528
+ emitDeadlineEvent: emitDeadlineEventEffect,
529
+ enqueueDeadlineCheck: enqueueDeadlineCheckEffect,
530
+ }
531
+ }
532
+
533
+ export class PlanDeadlineServiceTag extends Context.Service<
534
+ PlanDeadlineServiceTag,
535
+ ReturnType<typeof makePlanDeadlineService>
536
+ >()('PlanDeadlineService') {}
537
+
538
+ export const PlanDeadlineServiceLive = Layer.effect(
539
+ PlanDeadlineServiceTag,
540
+ Effect.gen(function* () {
541
+ const db = yield* DatabaseServiceTag
542
+ const planExecutorService = yield* PlanExecutorServiceTag
543
+ const planEventDeliveryService = yield* PlanEventDeliveryServiceTag
544
+ const planRunService = yield* PlanRunServiceTag
545
+ return makePlanDeadlineService({ db, planExecutorService, planEventDeliveryService, planRunService })
546
+ }),
547
+ )