@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.
- package/package.json +11 -12
- package/src/ai/embedding-cache.ts +96 -22
- package/src/ai-gateway/ai-gateway.ts +766 -223
- package/src/config/agent-defaults.ts +189 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/background-processing.ts +1 -1
- package/src/config/constants.ts +8 -2
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +299 -19
- package/src/config/thread-defaults.ts +40 -20
- package/src/create-runtime.ts +200 -449
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +868 -601
- package/src/db/memory.ts +396 -280
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +288 -0
- package/src/db/service.ts +912 -779
- package/src/db/startup.ts +153 -68
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +96 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +123 -0
- package/src/effect/index.ts +24 -0
- package/src/effect/layers.ts +238 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +46 -0
- package/src/effect/services.ts +61 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +128 -83
- package/src/index.ts +48 -1
- package/src/openrouter/direct-provider.ts +11 -35
- package/src/queues/autonomous-job.queue.ts +117 -73
- package/src/queues/context-compaction.queue.ts +50 -17
- package/src/queues/delayed-node-promotion.queue.ts +46 -17
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +26 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
- package/src/queues/plan-scheduler.queue.ts +97 -33
- package/src/queues/post-chat-memory.queue.ts +56 -26
- package/src/queues/queue-factory.ts +227 -59
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +45 -11
- package/src/redis/connection.ts +182 -113
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +20 -0
- package/src/redis/stream-context.ts +92 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +5 -2
- package/src/runtime/agent-stream-helpers.ts +24 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +16 -4
- package/src/runtime/helper-model.ts +139 -48
- package/src/runtime/index.ts +7 -8
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/memory/memory-scope.ts +53 -0
- package/src/runtime/plugin-resolution.ts +124 -25
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +177 -130
- package/src/runtime/retrieval-adapters.ts +40 -6
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +150 -61
- package/src/runtime/runtime-extensions.ts +23 -25
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +386 -0
- package/src/runtime/runtime-token.ts +47 -0
- package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
- package/src/runtime/social-chat/social-chat.ts +630 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +183 -111
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +253 -149
- package/src/services/artifact.service.ts +231 -149
- package/src/services/attachment.service.ts +171 -115
- package/src/services/autonomous-job.service.ts +890 -491
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +13 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +151 -88
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +278 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +101 -168
- package/src/services/graph-full-routing.ts +193 -0
- package/src/services/index.ts +19 -21
- package/src/services/institutional-memory.service.ts +213 -125
- package/src/services/learned-skill.service.ts +368 -260
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +50 -0
- package/src/services/memory/memory-preseeded.ts +86 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
- package/src/services/memory/memory.service.ts +674 -0
- package/src/services/memory/rerank.service.ts +201 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +29 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +153 -77
- package/src/services/ownership-dispatcher.service.ts +456 -263
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +169 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +405 -0
- package/src/services/plan/plan-deadline.service.ts +533 -0
- package/src/services/plan/plan-event-delivery.service.ts +266 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +522 -0
- package/src/services/plan/plan-executor-helpers.ts +307 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1737 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +637 -0
- package/src/services/plan/plan-scheduler.service.ts +379 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +36 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +131 -0
- package/src/services/plugin-executor.service.ts +102 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +288 -231
- package/src/services/recent-activity-title.service.ts +73 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +190 -122
- package/src/services/system-executor.service.ts +96 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +385 -0
- package/src/services/thread/thread-listing.ts +199 -0
- package/src/services/thread/thread-memory-block.ts +130 -0
- package/src/services/thread/thread-message.service.ts +379 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
- package/src/services/thread/thread-turn-streaming.ts +403 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +376 -0
- package/src/services/thread/thread.service.ts +344 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +334 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +3 -3
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +3 -3
- package/src/system-agents/memory.agent.ts +3 -3
- package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
- package/src/system-agents/skill-extractor.agent.ts +3 -3
- package/src/system-agents/skill-manager.agent.ts +3 -3
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +3 -3
- package/src/tools/execution-plan.tool.ts +241 -171
- package/src/tools/fetch-webpage.tool.ts +29 -18
- package/src/tools/firecrawl-client.ts +26 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +57 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +33 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +125 -84
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +25 -22
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +111 -20
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +164 -52
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +74 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/config/search.ts +0 -3
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/agent-types.ts +0 -1
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/memory-scope.ts +0 -43
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -914
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/rerank.service.ts +0 -156
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Clock, Deferred, Duration, Effect, Random, Schedule } from 'effect'
|
|
3
2
|
import type IORedis from 'ioredis'
|
|
4
3
|
|
|
4
|
+
import { LockAcquisitionError, LockLostError, RedisError } from '../effect/errors'
|
|
5
|
+
import { RedisServiceTag } from '../effect/services'
|
|
5
6
|
import { getErrorMessage } from '../utils/errors'
|
|
6
7
|
|
|
7
8
|
interface RedisLeaseLockLogger {
|
|
@@ -10,8 +11,8 @@ interface RedisLeaseLockLogger {
|
|
|
10
11
|
warn?: (message: string) => void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
interface RedisLeaseLockOptions {
|
|
14
|
-
redis
|
|
14
|
+
export interface RedisLeaseLockOptions {
|
|
15
|
+
redis?: IORedis
|
|
15
16
|
lockKey: string
|
|
16
17
|
lockTtlMs: number
|
|
17
18
|
retryDelayMs?: number
|
|
@@ -47,144 +48,222 @@ function log(logger: RedisLeaseLockLogger | undefined, level: keyof RedisLeaseLo
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
const waitStart = Date.now()
|
|
55
|
-
let lastWaitLogAt = waitStart
|
|
56
|
-
|
|
57
|
-
for (;;) {
|
|
58
|
-
const result = await options.redis.set(options.lockKey, options.lockValue, 'PX', options.lockTtlMs, 'NX')
|
|
59
|
-
|
|
60
|
-
if (result === 'OK') {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
51
|
+
function toRedisError(error: unknown, message: string): RedisError {
|
|
52
|
+
return new RedisError({ message: `${message}: ${getErrorMessage(error)}`, cause: error })
|
|
53
|
+
}
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
function getNormalizedOptions(options: RedisLeaseLockOptions) {
|
|
56
|
+
const normalizedLockTtlMs = Math.max(1_000, Math.trunc(options.lockTtlMs))
|
|
57
|
+
const requestedRefreshIntervalMs = options.refreshIntervalMs ?? Math.floor(normalizedLockTtlMs / 3)
|
|
58
|
+
const normalizedRefreshIntervalMs = Math.min(
|
|
59
|
+
Math.max(250, Math.trunc(requestedRefreshIntervalMs)),
|
|
60
|
+
Math.max(250, normalizedLockTtlMs - 250),
|
|
61
|
+
)
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
return {
|
|
64
|
+
...options,
|
|
65
|
+
lockKey: options.lockKey.trim(),
|
|
66
|
+
lockTtlMs: normalizedLockTtlMs,
|
|
67
|
+
retryDelayMs: options.retryDelayMs ?? 500,
|
|
68
|
+
refreshIntervalMs: normalizedRefreshIntervalMs,
|
|
69
|
+
waitLogIntervalMs: options.waitLogIntervalMs ?? 30_000,
|
|
70
|
+
maxWaitMs: options.maxWaitMs ?? 45_000,
|
|
71
|
+
acquiredWaitInfoThresholdMs: options.acquiredWaitInfoThresholdMs ?? 1_000,
|
|
72
|
+
heldInfoThresholdMs: options.heldInfoThresholdMs ?? 5_000,
|
|
73
|
+
label: options.label ?? 'redis lease lock',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
function resolveRedisConnection(options: RedisLeaseLockOptions): Effect.Effect<IORedis, RedisError, RedisServiceTag> {
|
|
78
|
+
if (options.redis) {
|
|
79
|
+
return Effect.succeed(options.redis)
|
|
76
80
|
}
|
|
81
|
+
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const redisService = yield* RedisServiceTag
|
|
84
|
+
return redisService.getConnection()
|
|
85
|
+
})
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
function acquireLock(
|
|
89
|
+
redis: IORedis,
|
|
90
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
91
|
+
lockValue: string,
|
|
92
|
+
): Effect.Effect<void, LockAcquisitionError | RedisError> {
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
const waitStart = yield* Clock.currentTimeMillis
|
|
95
|
+
let lastWaitLogAt = waitStart
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
const tryOnce = Effect.gen(function* () {
|
|
98
|
+
const result = yield* Effect.tryPromise({
|
|
99
|
+
try: () => redis.set(options.lockKey, lockValue, 'PX', options.lockTtlMs, 'NX'),
|
|
100
|
+
catch: (error) => toRedisError(error, `Failed to acquire ${options.label} (${options.lockKey})`),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (result === 'OK') {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const now = yield* Clock.currentTimeMillis
|
|
108
|
+
const waitedMs = now - waitStart
|
|
109
|
+
if (now - lastWaitLogAt >= options.waitLogIntervalMs) {
|
|
110
|
+
log(options.logger, 'info', `Waiting for ${options.label} (${options.lockKey}) waitedMs=${waitedMs}`)
|
|
111
|
+
lastWaitLogAt = now
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return yield* new LockAcquisitionError({ lockKey: options.lockKey, maxWaitMs: options.maxWaitMs })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return yield* Effect.retry(tryOnce, {
|
|
118
|
+
times: Math.max(0, Math.ceil(options.maxWaitMs / options.retryDelayMs) - 1),
|
|
119
|
+
schedule: Schedule.fixed(Duration.millis(options.retryDelayMs)),
|
|
120
|
+
}).pipe(
|
|
121
|
+
Effect.asVoid,
|
|
122
|
+
Effect.catchTag('LockAcquisitionError', () =>
|
|
123
|
+
Effect.fail(new LockAcquisitionError({ lockKey: options.lockKey, maxWaitMs: options.maxWaitMs })),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
})
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
function refreshLock(
|
|
130
|
+
redis: IORedis,
|
|
131
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
132
|
+
lockValue: string,
|
|
133
|
+
): Effect.Effect<void, LockLostError | RedisError> {
|
|
134
|
+
return Effect.tryPromise({
|
|
135
|
+
try: () => redis.eval(REFRESH_LOCK_SCRIPT, 1, options.lockKey, lockValue, options.lockTtlMs.toString()),
|
|
136
|
+
catch: (error) => toRedisError(error, `Failed to refresh ${options.label} (${options.lockKey})`),
|
|
137
|
+
}).pipe(
|
|
138
|
+
Effect.flatMap((refreshed) => {
|
|
139
|
+
const refreshedCount = typeof refreshed === 'number' ? refreshed : Number(refreshed)
|
|
140
|
+
if (refreshedCount === 1) {
|
|
141
|
+
return Effect.void
|
|
142
|
+
}
|
|
143
|
+
return Effect.fail(new LockLostError({ lockKey: options.lockKey }))
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
function releaseLock(
|
|
149
|
+
redis: IORedis,
|
|
150
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
151
|
+
lockValue: string,
|
|
152
|
+
): Effect.Effect<void, RedisError> {
|
|
153
|
+
return Effect.asVoid(
|
|
154
|
+
Effect.tryPromise({
|
|
155
|
+
try: () => redis.eval(RELEASE_LOCK_SCRIPT, 1, options.lockKey, lockValue),
|
|
156
|
+
catch: (error) => toRedisError(error, `Failed to release ${options.label} (${options.lockKey})`),
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
102
159
|
}
|
|
103
160
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ac.abort(new LeaseLockLostError(message))
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
}, options.refreshIntervalMs)
|
|
161
|
+
function startRefreshFiber(
|
|
162
|
+
redis: IORedis,
|
|
163
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
164
|
+
lockValue: string,
|
|
165
|
+
abortController: AbortController,
|
|
166
|
+
lockLost: Deferred.Deferred<never, LockLostError | RedisError>,
|
|
167
|
+
) {
|
|
168
|
+
const handleLockLoss = (error: LockLostError | RedisError) =>
|
|
169
|
+
Deferred.fail(lockLost, error).pipe(
|
|
170
|
+
Effect.andThen(
|
|
171
|
+
Effect.sync(() => {
|
|
172
|
+
const message =
|
|
173
|
+
error._tag === 'LockLostError'
|
|
174
|
+
? `${options.label} refresh was rejected for key ${options.lockKey}`
|
|
175
|
+
: error.message
|
|
123
176
|
|
|
124
|
-
|
|
177
|
+
log(options.logger, 'warn', message)
|
|
178
|
+
if (!abortController.signal.aborted) {
|
|
179
|
+
abortController.abort(error)
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
Effect.andThen(Effect.fail(error)),
|
|
184
|
+
)
|
|
125
185
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
const refreshProgram = Effect.gen(function* () {
|
|
187
|
+
for (;;) {
|
|
188
|
+
yield* Effect.sleep(Duration.millis(options.refreshIntervalMs))
|
|
189
|
+
|
|
190
|
+
yield* refreshLock(redis, options, lockValue).pipe(
|
|
191
|
+
Effect.catchTag('LockLostError', handleLockLoss),
|
|
192
|
+
Effect.catchTag('RedisError', handleLockLoss),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return Effect.forkScoped(refreshProgram).pipe(Effect.asVoid)
|
|
130
198
|
}
|
|
131
199
|
|
|
132
|
-
export
|
|
200
|
+
export function withLeaseLock<A, E, R>(
|
|
201
|
+
options: RedisLeaseLockOptions & { redis: IORedis },
|
|
202
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E, R>,
|
|
203
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError, R>
|
|
204
|
+
export function withLeaseLock<A, E, R>(
|
|
133
205
|
options: RedisLeaseLockOptions,
|
|
134
|
-
fn: (signal: AbortSignal) =>
|
|
135
|
-
):
|
|
136
|
-
const
|
|
137
|
-
if (!lockKey) {
|
|
138
|
-
throw new Error('Redis lease lock requires a non-empty lock key')
|
|
139
|
-
}
|
|
206
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E, R>,
|
|
207
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError, RedisServiceTag | R> {
|
|
208
|
+
const normalized = getNormalizedOptions(options)
|
|
140
209
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const acquiredWaitInfoThresholdMs = options.acquiredWaitInfoThresholdMs ?? 1_000
|
|
146
|
-
const heldInfoThresholdMs = options.heldInfoThresholdMs ?? 5_000
|
|
147
|
-
const label = options.label ?? 'redis lease lock'
|
|
148
|
-
|
|
149
|
-
const lockValue = crypto.randomUUID()
|
|
150
|
-
const waitStart = Date.now()
|
|
151
|
-
await acquireLeaseLock({ ...options, lockKey, lockValue, label, retryDelayMs, waitLogIntervalMs, maxWaitMs })
|
|
152
|
-
const waitedMs = Date.now() - waitStart
|
|
153
|
-
if (waitedMs >= acquiredWaitInfoThresholdMs) {
|
|
154
|
-
log(options.logger, 'info', `Acquired ${label} (${lockKey}) after waiting waitedMs=${waitedMs}`)
|
|
155
|
-
}
|
|
210
|
+
return Effect.gen(function* () {
|
|
211
|
+
if (!normalized.lockKey) {
|
|
212
|
+
return yield* new LockAcquisitionError({ lockKey: '', maxWaitMs: normalized.maxWaitMs })
|
|
213
|
+
}
|
|
156
214
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
215
|
+
const redis = yield* resolveRedisConnection(options)
|
|
216
|
+
const lockValue = yield* Random.nextUUIDv4
|
|
217
|
+
const waitStart = yield* Clock.currentTimeMillis
|
|
160
218
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
() => {
|
|
170
|
-
reject(ac.signal.reason as Error)
|
|
171
|
-
},
|
|
172
|
-
{ once: true },
|
|
219
|
+
yield* acquireLock(redis, normalized, lockValue)
|
|
220
|
+
|
|
221
|
+
const waitedMs = (yield* Clock.currentTimeMillis) - waitStart
|
|
222
|
+
if (waitedMs >= normalized.acquiredWaitInfoThresholdMs) {
|
|
223
|
+
log(
|
|
224
|
+
normalized.logger,
|
|
225
|
+
'info',
|
|
226
|
+
`Acquired ${normalized.label} (${normalized.lockKey}) after waiting waitedMs=${waitedMs}`,
|
|
173
227
|
)
|
|
174
|
-
})
|
|
175
|
-
return await Promise.race([fn(ac.signal), abortPromise])
|
|
176
|
-
} finally {
|
|
177
|
-
stopRefreshLoop()
|
|
178
|
-
const heldMs = Date.now() - holdStart
|
|
179
|
-
if (!ac.signal.aborted) {
|
|
180
|
-
await releaseLeaseLock({ ...options, lockKey, lockValue }).catch((error: unknown) => {
|
|
181
|
-
log(options.logger, 'warn', `Failed to release ${label} (${lockKey}): ${getErrorMessage(error)}`)
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
if (heldMs >= heldInfoThresholdMs) {
|
|
185
|
-
log(options.logger, 'info', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
|
|
186
|
-
} else {
|
|
187
|
-
log(options.logger, 'debug', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
|
|
188
228
|
}
|
|
189
|
-
|
|
229
|
+
|
|
230
|
+
const abortController = new AbortController()
|
|
231
|
+
const holdStart = yield* Clock.currentTimeMillis
|
|
232
|
+
const lockLost = yield* Deferred.make<never, LockLostError | RedisError>()
|
|
233
|
+
|
|
234
|
+
const cleanup = Effect.sync(() => {
|
|
235
|
+
if (!abortController.signal.aborted) {
|
|
236
|
+
abortController.abort()
|
|
237
|
+
}
|
|
238
|
+
}).pipe(
|
|
239
|
+
Effect.andThen(
|
|
240
|
+
releaseLock(redis, normalized, lockValue).pipe(
|
|
241
|
+
Effect.catch((error) =>
|
|
242
|
+
Effect.sync(() => {
|
|
243
|
+
log(normalized.logger, 'warn', error.message)
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
Effect.andThen(
|
|
249
|
+
Effect.gen(function* () {
|
|
250
|
+
const heldMs = (yield* Clock.currentTimeMillis) - holdStart
|
|
251
|
+
if (heldMs >= normalized.heldInfoThresholdMs) {
|
|
252
|
+
log(normalized.logger, 'info', `Released ${normalized.label} (${normalized.lockKey}) heldMs=${heldMs}`)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
log(normalized.logger, 'debug', `Released ${normalized.label} (${normalized.lockKey}) heldMs=${heldMs}`)
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
Effect.ignore,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return yield* Effect.scoped(
|
|
263
|
+
startRefreshFiber(redis, normalized, lockValue, abortController, lockLost).pipe(
|
|
264
|
+
Effect.andThen(fn(abortController.signal).pipe(Effect.raceFirst(Deferred.await(lockLost)))),
|
|
265
|
+
Effect.ensuring(cleanup),
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
})
|
|
190
269
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type IORedis from 'ioredis'
|
|
2
|
+
|
|
3
|
+
import { resolveLotaService } from '../effect/runtime'
|
|
4
|
+
import { RedisServiceTag } from '../effect/services'
|
|
5
|
+
import type { RedisConnectionManager } from './connection'
|
|
6
|
+
|
|
7
|
+
let currentRedisManager: RedisConnectionManager | null = null
|
|
8
|
+
|
|
9
|
+
export function configureRuntimeRedisManager(redisManager: RedisConnectionManager): void {
|
|
10
|
+
currentRedisManager = redisManager
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clearRuntimeRedisManager(): void {
|
|
14
|
+
currentRedisManager = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getRuntimeRedisConnection(): IORedis {
|
|
18
|
+
const redis = currentRedisManager ?? resolveLotaService(RedisServiceTag)
|
|
19
|
+
return redis.getConnection()
|
|
20
|
+
}
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
+
import { Context, Schema, Effect, Fiber, Layer, Queue, Stream } from 'effect'
|
|
1
2
|
import type { Redis } from 'ioredis'
|
|
2
3
|
import type { Publisher, Subscriber } from 'resumable-stream/ioredis'
|
|
3
4
|
import { createResumableStreamContext } from 'resumable-stream/ioredis'
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { serverLogger } from '../config/logger'
|
|
7
|
+
import { RedisServiceTag } from '../effect/services'
|
|
8
|
+
import type { RedisConnectionManager } from './connection'
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
const handlers = new Map<string, (message: string) => void>()
|
|
9
|
-
const messageListener = (channel: string, message: string) => {
|
|
10
|
-
handlers.get(channel)?.(message)
|
|
11
|
-
}
|
|
12
|
-
return {
|
|
13
|
-
connect: () => Promise.resolve(),
|
|
14
|
-
subscribe: async (channel, callback) => {
|
|
15
|
-
if (handlers.size === 0) client.on('message', messageListener)
|
|
16
|
-
handlers.set(channel, callback)
|
|
17
|
-
await client.subscribe(channel)
|
|
18
|
-
},
|
|
19
|
-
unsubscribe: async (channel) => {
|
|
20
|
-
handlers.delete(channel)
|
|
21
|
-
if (handlers.size === 0) client.removeListener('message', messageListener)
|
|
22
|
-
return client.unsubscribe(channel)
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
}
|
|
10
|
+
const SUBSCRIBER_QUEUE_CAPACITY = 1024
|
|
26
11
|
|
|
27
12
|
function toPublisher(client: Redis): Publisher {
|
|
28
13
|
return {
|
|
@@ -34,35 +19,96 @@ function toPublisher(client: Redis): Publisher {
|
|
|
34
19
|
}
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
|
|
22
|
+
type SharedSubscriberEvent = { readonly channel: string; readonly message: string }
|
|
23
|
+
|
|
24
|
+
class SharedSubscriberCloseError extends Schema.TaggedErrorClass<SharedSubscriberCloseError>()(
|
|
25
|
+
'SharedSubscriberCloseError',
|
|
26
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
export class SharedThreadStreamSubscriberTag extends Context.Service<
|
|
30
|
+
SharedThreadStreamSubscriberTag,
|
|
31
|
+
{ readonly subscriber: Subscriber }
|
|
32
|
+
>()('@lota-sdk/core/SharedThreadStreamSubscriber') {}
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
export const SharedThreadStreamSubscriberLive = Layer.effect(
|
|
35
|
+
SharedThreadStreamSubscriberTag,
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const redisManager = yield* RedisServiceTag
|
|
41
38
|
// Disable enableReadyCheck — the ready check sends INFO which is rejected
|
|
42
39
|
// on connections in subscribe mode, causing unhandled ioredis error events.
|
|
43
|
-
const client =
|
|
44
|
-
client.on('error', () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return sharedSubscriber.subscriber
|
|
48
|
-
}
|
|
40
|
+
const client = redisManager.getConnection().duplicate({ enableReadyCheck: false })
|
|
41
|
+
client.on('error', (error: unknown) => {
|
|
42
|
+
serverLogger.warn('Redis subscriber error', { cause: error })
|
|
43
|
+
})
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
const events = yield* Queue.bounded<SharedSubscriberEvent>(SUBSCRIBER_QUEUE_CAPACITY)
|
|
46
|
+
const handlers = new Map<string, (message: string) => void>()
|
|
47
|
+
|
|
48
|
+
const dispatchFiber = yield* Effect.forkScoped(
|
|
49
|
+
Stream.fromQueue(events).pipe(
|
|
50
|
+
Stream.runForEach((event) =>
|
|
51
|
+
Effect.sync(() => {
|
|
52
|
+
handlers.get(event.channel)?.(event.message)
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const messageListener = (channel: string, message: string) => {
|
|
59
|
+
// Synchronous bounded enqueue — drops (returns false) when consumer is slow,
|
|
60
|
+
// surfacing back-pressure instead of growing memory unbounded.
|
|
61
|
+
const accepted = Queue.offerUnsafe(events, { channel, message })
|
|
62
|
+
if (!accepted) {
|
|
63
|
+
serverLogger.warn('Redis subscriber queue full, dropping message', { channel })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
client.on('message', messageListener)
|
|
67
|
+
|
|
68
|
+
const subscribe = (channel: string, callback: (message: string) => void): Promise<void> => {
|
|
69
|
+
handlers.set(channel, callback)
|
|
70
|
+
return client.subscribe(channel).then(() => undefined)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const unsubscribe = (channel: string): Promise<void> => {
|
|
74
|
+
handlers.delete(channel)
|
|
75
|
+
return client.unsubscribe(channel).then(() => undefined)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const subscriber: Subscriber = {
|
|
79
|
+
connect: () => Promise.resolve(),
|
|
80
|
+
subscribe: (channel, callback) => subscribe(channel, callback),
|
|
81
|
+
unsubscribe: (channel) => unsubscribe(channel),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
yield* Effect.addFinalizer(() =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
handlers.clear()
|
|
87
|
+
client.removeListener('message', messageListener)
|
|
88
|
+
yield* Fiber.interrupt(dispatchFiber)
|
|
89
|
+
yield* Queue.shutdown(events)
|
|
90
|
+
yield* Effect.tryPromise({
|
|
91
|
+
try: () => client.quit(),
|
|
92
|
+
catch: (cause) =>
|
|
93
|
+
new SharedSubscriberCloseError({ message: 'Failed to quit shared stream subscriber.', cause }),
|
|
94
|
+
}).pipe(
|
|
95
|
+
Effect.catch(() =>
|
|
96
|
+
Effect.sync(() => {
|
|
97
|
+
client.disconnect()
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return { subscriber }
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
60
107
|
|
|
61
|
-
export function createThreadResumableContext(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
})
|
|
108
|
+
export function createThreadResumableContext(
|
|
109
|
+
redisManager: Pick<RedisConnectionManager, 'getConnection'>,
|
|
110
|
+
subscriber: Subscriber,
|
|
111
|
+
) {
|
|
112
|
+
const redis = redisManager.getConnection()
|
|
113
|
+
return createResumableStreamContext({ waitUntil: null, subscriber, publisher: toPublisher(redis) })
|
|
68
114
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAgentDisplayNames } from '../config/agent-defaults'
|
|
2
2
|
import { asRecord, readOptionalString } from './thread-chat-helpers'
|
|
3
3
|
|
|
4
4
|
interface RuntimeAgentIdentityOverrides {
|
|
@@ -58,5 +58,5 @@ export function resolveRuntimeAgentDisplayName(overrides: RuntimeAgentIdentityOv
|
|
|
58
58
|
return override
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
return
|
|
61
|
+
return getAgentDisplayNames()[agentId] ?? agentId
|
|
62
62
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ChatMode,
|
|
2
3
|
ExecutionMode,
|
|
3
4
|
OwnershipDispatchContext,
|
|
4
5
|
PlanArtifactSubmission,
|
|
@@ -8,7 +9,6 @@ import type {
|
|
|
8
9
|
|
|
9
10
|
import { getLeadAgentId } from '../config/agent-defaults'
|
|
10
11
|
import { resolveOnboardingOwnerAgentId } from '../config/thread-defaults'
|
|
11
|
-
import type { ChatMode } from './agent-types'
|
|
12
12
|
export interface AgentRuntimeConfig<TAgent extends string> {
|
|
13
13
|
id: TAgent
|
|
14
14
|
displayName: string
|
|
@@ -162,10 +162,13 @@ export function buildThreadAgentToolPolicy<TAgent extends string, TSkill extends
|
|
|
162
162
|
linearInstalled: boolean
|
|
163
163
|
githubInstalled: boolean
|
|
164
164
|
provideRepoTool: boolean
|
|
165
|
+
leadAgentId?: TAgent
|
|
166
|
+
onboardingOwnerAgentId?: TAgent
|
|
165
167
|
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
166
168
|
}): AgentToolPolicy<TSkill> {
|
|
167
169
|
const resolvedMode = params.mode ?? toChatMode(params.threadType)
|
|
168
|
-
const
|
|
170
|
+
const leadAgentId = params.leadAgentId ?? (getLeadAgentId() as TAgent)
|
|
171
|
+
const onboardingOwnerAgentId = params.onboardingOwnerAgentId ?? (resolveOnboardingOwnerAgentId(leadAgentId) as TAgent)
|
|
169
172
|
const skills = resolveActiveAgentSkills({
|
|
170
173
|
agentId: params.agentId,
|
|
171
174
|
threadType: params.threadType,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
2
2
|
import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
|
|
3
|
+
import { Duration, Effect, Exit, Scope } from 'effect'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { getAgentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
|
|
6
|
+
import { nowEpochMillis } from '../utils/date-time'
|
|
5
7
|
import { readRecord as _readRecord } from '../utils/string'
|
|
6
8
|
|
|
7
9
|
export function readFiniteNumber(value: unknown): number | undefined {
|
|
@@ -32,7 +34,7 @@ export function createAgentMessageMetadata(params: {
|
|
|
32
34
|
}): NonNullable<UIMessageStreamOptions<ChatMessage>['messageMetadata']> {
|
|
33
35
|
return ({ part }) => {
|
|
34
36
|
if (part.type === 'start') {
|
|
35
|
-
return { agentId: params.agentId, agentName: params.agentName, createdAt:
|
|
37
|
+
return { agentId: params.agentId, agentName: params.agentName, createdAt: nowEpochMillis() }
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
if (part.type === 'finish') {
|
|
@@ -100,13 +102,24 @@ export function createServerRunAbortController(externalAbortSignal?: AbortSignal
|
|
|
100
102
|
export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: number) {
|
|
101
103
|
const controller = new AbortController()
|
|
102
104
|
let didTimeout = false
|
|
105
|
+
let disposed = false
|
|
103
106
|
const abortFromParent = () => {
|
|
104
107
|
controller.abort((parentSignal as AbortSignal & { reason?: unknown }).reason)
|
|
105
108
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
|
|
110
|
+
// Own a scope so the timeout fiber is cleaned up via Scope.close() on dispose,
|
|
111
|
+
// matching Effect v4 best practices (forkScoped + scope-bound finalizers).
|
|
112
|
+
const scope = Scope.makeUnsafe()
|
|
113
|
+
const timeoutEffect = Effect.sleep(Duration.millis(timeoutMs)).pipe(
|
|
114
|
+
Effect.andThen(
|
|
115
|
+
Effect.sync(() => {
|
|
116
|
+
if (disposed || controller.signal.aborted) return
|
|
117
|
+
didTimeout = true
|
|
118
|
+
controller.abort(new Error(`Timed out after ${timeoutMs}ms`))
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
Effect.runFork(Scope.provide(Effect.forkScoped(timeoutEffect), scope))
|
|
110
123
|
|
|
111
124
|
if (parentSignal.aborted) {
|
|
112
125
|
abortFromParent()
|
|
@@ -118,14 +131,16 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
|
|
|
118
131
|
signal: controller.signal,
|
|
119
132
|
didTimeout: () => didTimeout,
|
|
120
133
|
dispose: () => {
|
|
121
|
-
|
|
134
|
+
if (disposed) return
|
|
135
|
+
disposed = true
|
|
122
136
|
parentSignal.removeEventListener('abort', abortFromParent)
|
|
137
|
+
void Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
|
|
123
138
|
},
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
export function buildSpecialistTaskMessage(params: { agentId: string; task: string }): ChatMessage {
|
|
128
|
-
const displayName =
|
|
143
|
+
const displayName = getAgentDisplayNames()[params.agentId] ?? params.agentId
|
|
129
144
|
const leadAgentDisplayName = getLeadAgentDisplayName()
|
|
130
145
|
return {
|
|
131
146
|
id: Bun.randomUUIDv7(),
|
|
@@ -133,6 +148,6 @@ export function buildSpecialistTaskMessage(params: { agentId: string; task: stri
|
|
|
133
148
|
parts: [
|
|
134
149
|
{ type: 'text', text: [`${leadAgentDisplayName} request for ${displayName}:`, params.task.trim()].join('\n') },
|
|
135
150
|
],
|
|
136
|
-
metadata: { createdAt:
|
|
151
|
+
metadata: { createdAt: nowEpochMillis() },
|
|
137
152
|
}
|
|
138
153
|
}
|