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