@lota-sdk/core 0.4.9 → 0.4.11
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 +2 -2
- package/src/ai/embedding-cache.ts +3 -1
- package/src/ai-gateway/ai-gateway.ts +164 -82
- package/src/ai-gateway/index.ts +16 -1
- package/src/config/agent-defaults.ts +4 -107
- package/src/config/agent-types.ts +1 -1
- package/src/config/background-processing.ts +1 -1
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +22 -25
- package/src/config/thread-defaults.ts +1 -10
- package/src/create-runtime.ts +145 -670
- package/src/db/base.service.ts +30 -38
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.ts +29 -20
- package/src/db/memory.ts +188 -195
- package/src/db/service-normalization.ts +97 -64
- package/src/db/service.ts +496 -384
- package/src/db/startup.ts +30 -19
- package/src/effect/helpers.ts +30 -5
- package/src/effect/index.ts +7 -7
- package/src/effect/layers.ts +75 -72
- package/src/effect/services.ts +15 -11
- package/src/embeddings/provider.ts +65 -71
- package/src/index.ts +13 -12
- package/src/queues/autonomous-job.queue.ts +177 -143
- package/src/queues/context-compaction.queue.ts +41 -39
- package/src/queues/delayed-node-promotion.queue.ts +61 -42
- package/src/queues/document-processor.queue.ts +5 -3
- package/src/queues/index.ts +1 -0
- package/src/queues/memory-consolidation.queue.ts +79 -53
- package/src/queues/organization-learning.queue.ts +70 -33
- package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
- package/src/queues/plan-scheduler.queue.ts +101 -97
- package/src/queues/post-chat-memory.queue.ts +56 -46
- package/src/queues/queue-factory.ts +146 -69
- package/src/queues/queues.service.ts +61 -0
- package/src/queues/title-generation.queue.ts +44 -44
- package/src/redis/connection.ts +181 -164
- package/src/redis/org-memory-lock.ts +24 -9
- package/src/redis/redis-lease-lock.ts +8 -1
- package/src/redis/stream-context.ts +17 -9
- package/src/runtime/agent-identity-overrides.ts +7 -3
- package/src/runtime/agent-runtime-policy.ts +10 -5
- package/src/runtime/agent-stream-helpers.ts +24 -15
- package/src/runtime/chat-run-orchestration.ts +1 -1
- package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
- package/src/runtime/context-compaction/context-compaction.ts +131 -85
- package/src/runtime/domain-layer.ts +203 -0
- package/src/runtime/execution-plan-visibility.ts +5 -2
- package/src/runtime/graph-designer.ts +0 -14
- package/src/runtime/helper-model.ts +8 -4
- package/src/runtime/index.ts +1 -1
- package/src/runtime/indexed-repositories-policy.ts +2 -6
- package/src/runtime/memory/memory-block.ts +19 -9
- package/src/runtime/memory/memory-pipeline.ts +53 -66
- package/src/runtime/memory/memory-scope.ts +33 -29
- package/src/runtime/plugin-resolution.ts +58 -62
- package/src/runtime/post-turn-side-effects.ts +139 -161
- package/src/runtime/retrieval-adapters.ts +4 -4
- package/src/runtime/runtime-config.ts +3 -9
- package/src/runtime/runtime-extensions.ts +0 -43
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +455 -0
- package/src/runtime/runtime-worker-registry.ts +113 -30
- package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
- package/src/runtime/social-chat/social-chat-history.ts +24 -13
- package/src/runtime/social-chat/social-chat.ts +420 -369
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
- package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
- package/src/runtime/thread-chat-helpers.ts +18 -9
- package/src/runtime/thread-turn-context.ts +28 -74
- package/src/runtime/turn-lifecycle.ts +6 -14
- package/src/services/agent-activity.service.ts +169 -176
- package/src/services/agent-executor.service.ts +207 -196
- package/src/services/artifact.service.ts +10 -5
- package/src/services/attachment.service.ts +16 -48
- package/src/services/autonomous-job.service.ts +81 -87
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +3 -1
- package/src/services/context-compaction.service.ts +8 -10
- package/src/services/document-chunk.service.ts +8 -17
- package/src/services/execution-plan/execution-plan-graph.ts +122 -109
- package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
- package/src/services/execution-plan/execution-plan.service.ts +68 -51
- package/src/services/feedback-loop.service.ts +1 -1
- package/src/services/global-orchestrator.service.ts +49 -15
- package/src/services/graph-full-routing.ts +49 -37
- package/src/services/index.ts +1 -0
- package/src/services/institutional-memory.service.ts +8 -17
- package/src/services/learned-skill.service.ts +38 -35
- package/src/services/memory/memory-conversation.ts +10 -5
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +14 -3
- package/src/services/memory/memory-preseeded.ts +10 -4
- package/src/services/memory/memory-utils.ts +2 -1
- package/src/services/memory/memory.service.ts +37 -52
- package/src/services/memory/rerank.service.ts +3 -11
- package/src/services/monitoring-window.service.ts +1 -1
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +2 -2
- package/src/services/notification.service.ts +16 -4
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +34 -51
- package/src/services/ownership-dispatcher.service.ts +148 -95
- package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
- package/src/services/plan/plan-agent-query.service.ts +13 -9
- package/src/services/plan/plan-approval.service.ts +52 -48
- package/src/services/plan/plan-artifact.service.ts +2 -2
- package/src/services/plan/plan-builder.service.ts +2 -2
- package/src/services/plan/plan-checkpoint.service.ts +1 -1
- package/src/services/plan/plan-compiler.service.ts +1 -1
- package/src/services/plan/plan-completion-side-effects.ts +99 -113
- package/src/services/plan/plan-coordination.service.ts +1 -1
- package/src/services/plan/plan-cycle.service.ts +171 -202
- package/src/services/plan/plan-deadline.service.ts +304 -307
- package/src/services/plan/plan-event-delivery.service.ts +84 -72
- package/src/services/plan/plan-executor-context.ts +2 -0
- package/src/services/plan/plan-executor-graph.ts +375 -353
- package/src/services/plan/plan-executor-helpers.ts +60 -75
- package/src/services/plan/plan-executor.service.ts +494 -489
- package/src/services/plan/plan-run.service.ts +12 -19
- package/src/services/plan/plan-scheduler.service.ts +89 -82
- package/src/services/plan/plan-template.service.ts +1 -1
- package/src/services/plan/plan-transaction-events.ts +8 -5
- package/src/services/plan/plan-validator.service.ts +1 -1
- package/src/services/plan/plan-workspace.service.ts +17 -11
- package/src/services/plugin-executor.service.ts +26 -21
- package/src/services/quality-metrics.service.ts +1 -1
- package/src/services/queue-job.service.ts +8 -17
- package/src/services/recent-activity-title.service.ts +22 -10
- package/src/services/recent-activity.service.ts +1 -1
- package/src/services/skill-resolver.service.ts +1 -1
- package/src/services/social-chat-history.service.ts +37 -20
- package/src/services/system-executor.service.ts +25 -20
- package/src/services/thread/thread-bootstrap.ts +37 -19
- package/src/services/thread/thread-listing.ts +2 -1
- package/src/services/thread/thread-memory-block.ts +18 -5
- package/src/services/thread/thread-message.service.ts +30 -13
- package/src/services/thread/thread-title.service.ts +1 -1
- package/src/services/thread/thread-turn-execution.ts +87 -83
- package/src/services/thread/thread-turn-preparation.service.ts +65 -40
- package/src/services/thread/thread-turn-streaming.ts +32 -36
- package/src/services/thread/thread-turn.ts +43 -29
- package/src/services/thread/thread.service.ts +32 -8
- package/src/services/user.service.ts +1 -1
- package/src/services/write-intent-validator.service.ts +1 -1
- package/src/storage/attachment-storage.service.ts +7 -4
- package/src/storage/generated-document-storage.service.ts +1 -1
- package/src/system-agents/context-compaction.agent.ts +1 -1
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/skill-extractor.agent.ts +1 -1
- package/src/system-agents/skill-manager.agent.ts +1 -1
- package/src/system-agents/thread-router.agent.ts +23 -20
- package/src/system-agents/title-generator.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +36 -20
- package/src/tools/fetch-webpage.tool.ts +30 -22
- package/src/tools/firecrawl-client.ts +1 -6
- package/src/tools/plan-approval.tool.ts +9 -1
- package/src/tools/remember-memory.tool.ts +3 -6
- package/src/tools/research-topic.tool.ts +12 -3
- package/src/tools/search-web.tool.ts +26 -18
- package/src/tools/search.tool.ts +4 -5
- package/src/tools/team-think.tool.ts +139 -121
- package/src/utils/async.ts +15 -6
- package/src/utils/errors.ts +27 -15
- package/src/workers/bootstrap.ts +34 -58
- package/src/workers/memory-consolidation.worker.ts +4 -1
- package/src/workers/organization-learning.worker.ts +16 -3
- package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
- package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
- package/src/workers/skill-extraction.runner.ts +13 -15
- package/src/workers/worker-utils.ts +14 -8
- package/src/config/search.ts +0 -3
- package/src/effect/awaitable-effect.ts +0 -87
- package/src/effect/runtime-ref.ts +0 -25
- package/src/effect/runtime.ts +0 -31
- package/src/redis/runtime-connection.ts +0 -10
- package/src/runtime/agent-types.ts +0 -1
package/src/redis/connection.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Duration, Effect, Exit,
|
|
1
|
+
import { Duration, Effect, Exit, Scope } from 'effect'
|
|
2
2
|
import IORedis from 'ioredis'
|
|
3
3
|
import type { RedisOptions } from 'ioredis'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { RedisError } from '../effect/errors'
|
|
6
6
|
import { effectTryServicePromise } from '../effect/helpers'
|
|
7
7
|
import { getErrorMessage } from '../utils/errors'
|
|
8
8
|
|
|
@@ -32,8 +32,6 @@ const REDIS_RETRY_STEP_MS = 50
|
|
|
32
32
|
const REDIS_RETRY_MAX_DELAY_MS = 2000
|
|
33
33
|
const REDIS_CONNECT_TIMEOUT_MS = 10_000
|
|
34
34
|
|
|
35
|
-
type HealthCheckFiber = ReturnType<typeof Effect.runFork>
|
|
36
|
-
|
|
37
35
|
export const DEFAULT_REDIS_OPTIONS: RedisOptions = {
|
|
38
36
|
maxRetriesPerRequest: null,
|
|
39
37
|
enableReadyCheck: true,
|
|
@@ -57,181 +55,200 @@ function log(logger: RedisConnectionLogger | undefined, level: keyof RedisConnec
|
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
private isClosing = false
|
|
65
|
-
private isHealthCheckRunning = false
|
|
66
|
-
private healthChecksActive = false
|
|
67
|
-
private healthCheckFiber: HealthCheckFiber | null = null
|
|
68
|
-
|
|
69
|
-
constructor(private readonly options: CreateRedisConnectionManagerOptions) {
|
|
70
|
-
this.initializeConnection()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private initializeConnection(): void {
|
|
74
|
-
if (this.isInitialized) {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this.isInitialized = true
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const redisOptions = this.options.redisOptions ?? DEFAULT_REDIS_OPTIONS
|
|
82
|
-
this.redis = new IORedis(this.options.url, redisOptions)
|
|
83
|
-
this.setupEventHandlers()
|
|
84
|
-
this.startHealthChecks()
|
|
85
|
-
} catch (error) {
|
|
86
|
-
log(this.options.logger, 'error', `Failed to initialize Redis connection: ${getErrorMessage(error)}`)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private setupEventHandlers(): void {
|
|
91
|
-
if (!this.redis) return
|
|
92
|
-
|
|
93
|
-
this.redis.on('connect', () => {
|
|
94
|
-
log(this.options.logger, 'info', 'Redis connected')
|
|
95
|
-
this.isHealthy = true
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
this.redis.on('ready', () => {
|
|
99
|
-
log(this.options.logger, 'info', 'Redis ready')
|
|
100
|
-
this.isHealthy = true
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
this.redis.on('error', (error: Error) => {
|
|
104
|
-
log(this.options.logger, 'error', `Redis error: ${error.message}`)
|
|
105
|
-
this.isHealthy = false
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
this.redis.on('close', () => {
|
|
109
|
-
log(
|
|
110
|
-
this.options.logger,
|
|
111
|
-
this.isClosing ? 'info' : 'warn',
|
|
112
|
-
this.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
|
|
113
|
-
)
|
|
114
|
-
this.isHealthy = false
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
this.redis.on('reconnecting', () => {
|
|
118
|
-
log(this.options.logger, 'info', 'Redis reconnecting...')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
this.redis.on('end', () => {
|
|
122
|
-
log(
|
|
123
|
-
this.options.logger,
|
|
124
|
-
this.isClosing ? 'info' : 'warn',
|
|
125
|
-
this.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
|
|
126
|
-
)
|
|
127
|
-
this.isHealthy = false
|
|
128
|
-
})
|
|
129
|
-
}
|
|
58
|
+
interface ManagerState {
|
|
59
|
+
isHealthy: boolean
|
|
60
|
+
isClosing: boolean
|
|
61
|
+
}
|
|
130
62
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build the IORedis client + listeners as an `Effect.acquireRelease`. Each
|
|
65
|
+
* handler is held as a const ref so the release step can pass the same
|
|
66
|
+
* function to `client.off(...)`, then `quit()` (falling back to
|
|
67
|
+
* `disconnect()`) the client. Prevents tracked-handler leaks on disposal.
|
|
68
|
+
*/
|
|
69
|
+
function acquireRedisClient(
|
|
70
|
+
options: CreateRedisConnectionManagerOptions,
|
|
71
|
+
state: ManagerState,
|
|
72
|
+
): Effect.Effect<IORedis, RedisError, Scope.Scope> {
|
|
73
|
+
return Effect.acquireRelease(
|
|
74
|
+
Effect.try({
|
|
75
|
+
try: () => {
|
|
76
|
+
const redisOptions = options.redisOptions ?? DEFAULT_REDIS_OPTIONS
|
|
77
|
+
const client = new IORedis(options.url, redisOptions)
|
|
78
|
+
|
|
79
|
+
const onConnect = () => {
|
|
80
|
+
log(options.logger, 'info', 'Redis connected')
|
|
81
|
+
state.isHealthy = true
|
|
82
|
+
}
|
|
83
|
+
const onReady = () => {
|
|
84
|
+
log(options.logger, 'info', 'Redis ready')
|
|
85
|
+
state.isHealthy = true
|
|
86
|
+
}
|
|
87
|
+
const onError = (error: Error) => {
|
|
88
|
+
log(options.logger, 'error', `Redis error: ${error.message}`)
|
|
89
|
+
state.isHealthy = false
|
|
90
|
+
}
|
|
91
|
+
const onClose = () => {
|
|
92
|
+
log(
|
|
93
|
+
options.logger,
|
|
94
|
+
state.isClosing ? 'info' : 'warn',
|
|
95
|
+
state.isClosing ? 'Redis connection closed during shutdown' : 'Redis connection closed',
|
|
96
|
+
)
|
|
97
|
+
state.isHealthy = false
|
|
98
|
+
}
|
|
99
|
+
const onReconnecting = () => {
|
|
100
|
+
log(options.logger, 'info', 'Redis reconnecting...')
|
|
101
|
+
}
|
|
102
|
+
const onEnd = () => {
|
|
103
|
+
log(
|
|
104
|
+
options.logger,
|
|
105
|
+
state.isClosing ? 'info' : 'warn',
|
|
106
|
+
state.isClosing ? 'Redis connection ended during shutdown' : 'Redis connection ended',
|
|
107
|
+
)
|
|
108
|
+
state.isHealthy = false
|
|
109
|
+
}
|
|
135
110
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
111
|
+
client.on('connect', onConnect)
|
|
112
|
+
client.on('ready', onReady)
|
|
113
|
+
client.on('error', onError)
|
|
114
|
+
client.on('close', onClose)
|
|
115
|
+
client.on('reconnecting', onReconnecting)
|
|
116
|
+
client.on('end', onEnd)
|
|
117
|
+
|
|
118
|
+
// Carry the handler refs alongside the client so the release closure
|
|
119
|
+
// can detach exactly what was attached.
|
|
120
|
+
;(client as ClientWithHandlers).__lotaHandlers = {
|
|
121
|
+
connect: onConnect,
|
|
122
|
+
ready: onReady,
|
|
123
|
+
error: onError,
|
|
124
|
+
close: onClose,
|
|
125
|
+
reconnecting: onReconnecting,
|
|
126
|
+
end: onEnd,
|
|
127
|
+
}
|
|
148
128
|
|
|
149
|
-
|
|
129
|
+
return client
|
|
130
|
+
},
|
|
131
|
+
catch: (cause) =>
|
|
132
|
+
new RedisError({ message: `Failed to initialize Redis connection: ${getErrorMessage(cause)}`, cause }),
|
|
133
|
+
}),
|
|
134
|
+
(client) =>
|
|
150
135
|
Effect.gen(function* () {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} finally {
|
|
171
|
-
setHealthCheckRunning(false)
|
|
136
|
+
state.isClosing = true
|
|
137
|
+
|
|
138
|
+
const handlers = (client as ClientWithHandlers).__lotaHandlers
|
|
139
|
+
if (handlers) {
|
|
140
|
+
client.off('connect', handlers.connect)
|
|
141
|
+
client.off('ready', handlers.ready)
|
|
142
|
+
client.off('error', handlers.error)
|
|
143
|
+
client.off('close', handlers.close)
|
|
144
|
+
client.off('reconnecting', handlers.reconnecting)
|
|
145
|
+
client.off('end', handlers.end)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (client.status !== 'end') {
|
|
149
|
+
const quitExit = yield* Effect.exit(
|
|
150
|
+
effectTryServicePromise(() => client.quit(), 'Failed to close Redis connection manager'),
|
|
151
|
+
)
|
|
152
|
+
if (Exit.isFailure(quitExit)) {
|
|
153
|
+
log(options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
|
|
154
|
+
client.disconnect()
|
|
172
155
|
}
|
|
173
156
|
}
|
|
174
157
|
}),
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
private stopHealthChecks(): Promise<void> {
|
|
179
|
-
this.healthChecksActive = false
|
|
180
|
-
|
|
181
|
-
const fiber = this.healthCheckFiber
|
|
182
|
-
if (!fiber) {
|
|
183
|
-
return Promise.resolve()
|
|
184
|
-
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
161
|
+
interface RedisHandlers {
|
|
162
|
+
readonly connect: () => void
|
|
163
|
+
readonly ready: () => void
|
|
164
|
+
readonly error: (error: Error) => void
|
|
165
|
+
readonly close: () => void
|
|
166
|
+
readonly reconnecting: () => void
|
|
167
|
+
readonly end: () => void
|
|
168
|
+
}
|
|
192
169
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
170
|
+
type ClientWithHandlers = IORedis & { __lotaHandlers?: RedisHandlers }
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Health-check loop wired via `Effect.forkScoped` so interruption is automatic
|
|
174
|
+
* when the owning scope closes. No more imperative fiber tracking.
|
|
175
|
+
*/
|
|
176
|
+
function startHealthCheckFiber(
|
|
177
|
+
client: IORedis,
|
|
178
|
+
options: CreateRedisConnectionManagerOptions,
|
|
179
|
+
state: ManagerState,
|
|
180
|
+
): Effect.Effect<void, never, Scope.Scope> {
|
|
181
|
+
const intervalMs = options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
|
|
182
|
+
const logger = options.logger
|
|
183
|
+
let isHealthCheckRunning = false
|
|
184
|
+
|
|
185
|
+
const loop = Effect.gen(function* () {
|
|
186
|
+
for (;;) {
|
|
187
|
+
yield* Effect.sleep(Duration.millis(intervalMs))
|
|
188
|
+
if (isHealthCheckRunning) continue
|
|
189
|
+
|
|
190
|
+
isHealthCheckRunning = true
|
|
191
|
+
try {
|
|
192
|
+
if (client.status === 'ready') {
|
|
193
|
+
const pingExit = yield* Effect.exit(effectTryServicePromise(() => client.ping(), 'Redis health check failed'))
|
|
194
|
+
if (Exit.isFailure(pingExit)) {
|
|
195
|
+
log(logger, 'warn', `Redis health check failed: ${getErrorMessage(pingExit.cause)}`)
|
|
196
|
+
state.isHealthy = false
|
|
197
|
+
} else {
|
|
198
|
+
state.isHealthy = true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
isHealthCheckRunning = false
|
|
203
|
+
}
|
|
196
204
|
}
|
|
197
|
-
|
|
198
|
-
}
|
|
205
|
+
})
|
|
199
206
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
isConnectionHealthy(): boolean {
|
|
205
|
-
return this.isHealthy && this.redis?.status === 'ready'
|
|
206
|
-
}
|
|
207
|
+
return Effect.asVoid(Effect.forkScoped(loop))
|
|
208
|
+
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Scoped builder for the Redis connection manager. The IORedis client and
|
|
212
|
+
* health-check fiber are both bound to the surrounding scope, so closing the
|
|
213
|
+
* scope tears down listeners + quits the client + interrupts the fiber.
|
|
214
|
+
*/
|
|
215
|
+
export function makeRedisConnectionManager(
|
|
216
|
+
options: CreateRedisConnectionManagerOptions,
|
|
217
|
+
): Effect.Effect<RedisConnectionManager, RedisError, Scope.Scope> {
|
|
218
|
+
return Effect.gen(function* () {
|
|
219
|
+
const state: ManagerState = { isHealthy: false, isClosing: false }
|
|
220
|
+
const client = yield* acquireRedisClient(options, state)
|
|
221
|
+
yield* startHealthCheckFiber(client, options, state)
|
|
222
|
+
|
|
223
|
+
const manager: RedisConnectionManager = {
|
|
224
|
+
getConnection: () => client,
|
|
225
|
+
getConnectionForBullMQ: () => client,
|
|
226
|
+
isConnectionHealthy: () => state.isHealthy && client.status === 'ready',
|
|
227
|
+
closeConnection: () => Promise.resolve(),
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
yield* Effect.tryPromise(() => stopHealthChecks())
|
|
220
|
-
const redis = getRedis()
|
|
221
|
-
if (redis && redis.status !== 'end') {
|
|
222
|
-
const quitExit = yield* Effect.exit(
|
|
223
|
-
effectTryServicePromise(() => redis.quit(), 'Failed to close Redis connection manager'),
|
|
224
|
-
)
|
|
225
|
-
if (Exit.isFailure(quitExit)) {
|
|
226
|
-
log(logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
|
|
227
|
-
disconnect()
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}),
|
|
231
|
-
)
|
|
232
|
-
}
|
|
230
|
+
return manager
|
|
231
|
+
})
|
|
233
232
|
}
|
|
234
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Standalone Promise-friendly factory for hosts that build the manager outside
|
|
236
|
+
* a Layer. Owns its own `Scope`; `closeConnection()` closes that scope, which
|
|
237
|
+
* runs the same finalizer chain (listener removal, quit/disconnect, fiber
|
|
238
|
+
* interrupt) that `Layer.effect` runs at runtime shutdown.
|
|
239
|
+
*/
|
|
235
240
|
export function createRedisConnectionManager(options: CreateRedisConnectionManagerOptions): RedisConnectionManager {
|
|
236
|
-
|
|
241
|
+
const scope = Scope.makeUnsafe()
|
|
242
|
+
const built = Effect.runSync(Scope.provide(makeRedisConnectionManager(options), scope))
|
|
243
|
+
let closePromise: Promise<void> | null = null
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...built,
|
|
247
|
+
closeConnection: () => {
|
|
248
|
+
if (!closePromise) {
|
|
249
|
+
closePromise = Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
|
|
250
|
+
}
|
|
251
|
+
return closePromise
|
|
252
|
+
},
|
|
253
|
+
}
|
|
237
254
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Schema, Effect } from 'effect'
|
|
2
|
-
import type IORedis from 'ioredis'
|
|
3
2
|
|
|
4
3
|
import { serverLogger } from '../config/logger'
|
|
5
4
|
import { LockAcquisitionError } from '../effect/errors'
|
|
6
5
|
import type { LockLostError, RedisError } from '../effect/errors'
|
|
6
|
+
import { RedisServiceTag } from '../effect/services'
|
|
7
|
+
import type { RedisConnectionManager } from './connection'
|
|
7
8
|
import { withLeaseLock } from './redis-lease-lock'
|
|
8
9
|
import type { RedisLeaseLockOptions } from './redis-lease-lock'
|
|
9
|
-
import { getRuntimeRedisConnection } from './runtime-connection'
|
|
10
10
|
|
|
11
11
|
const ORG_MEMORY_LOCK_PREFIX = 'lock:org-memory:org:'
|
|
12
12
|
const ORG_MEMORY_LOCK_TTL_MS = 120_000
|
|
@@ -20,9 +20,8 @@ class OrgMemoryLockCallbackError extends Schema.TaggedErrorClass<OrgMemoryLockCa
|
|
|
20
20
|
{ message: Schema.String, cause: Schema.Defect },
|
|
21
21
|
) {}
|
|
22
22
|
|
|
23
|
-
function createOrgMemoryLockOptions(
|
|
23
|
+
function createOrgMemoryLockOptions(orgId: string): RedisLeaseLockOptions {
|
|
24
24
|
return {
|
|
25
|
-
redis,
|
|
26
25
|
lockKey: `${ORG_MEMORY_LOCK_PREFIX}${orgId}`,
|
|
27
26
|
lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
|
|
28
27
|
retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
|
|
@@ -44,13 +43,28 @@ function createOrgMemoryLockOptions(redis: IORedis, orgId: string): RedisLeaseLo
|
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Promise-returning wrapper for hosts that live outside Effect.
|
|
48
|
+
*
|
|
49
|
+
* Callers must pass in a `RedisConnectionManager` (usually
|
|
50
|
+
* `runtime.redis.manager`) so the wrapper can build a standalone Redis layer
|
|
51
|
+
* instead of relying on an ambient managed runtime.
|
|
52
|
+
*/
|
|
53
|
+
export function withOrgMemoryLock<T>(
|
|
54
|
+
orgId: string,
|
|
55
|
+
fn: (signal: AbortSignal) => Promise<T>,
|
|
56
|
+
redisManager: RedisConnectionManager,
|
|
57
|
+
): Promise<T> {
|
|
48
58
|
return Effect.runPromise(
|
|
49
59
|
withOrgMemoryLockEffect(orgId, (signal) =>
|
|
50
60
|
Effect.tryPromise({
|
|
51
61
|
try: () => fn(signal),
|
|
52
62
|
catch: (cause) => new OrgMemoryLockCallbackError({ message: 'Org memory lock callback failed.', cause }),
|
|
53
63
|
}),
|
|
64
|
+
).pipe(
|
|
65
|
+
// Provide the redis service synchronously so the Effect has no residual
|
|
66
|
+
// requirements; the outer Promise call edge needs `R = never`.
|
|
67
|
+
Effect.provideService(RedisServiceTag, redisManager),
|
|
54
68
|
),
|
|
55
69
|
)
|
|
56
70
|
}
|
|
@@ -58,7 +72,7 @@ export function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) =>
|
|
|
58
72
|
export function withOrgMemoryLockEffect<A, E>(
|
|
59
73
|
orgId: string,
|
|
60
74
|
fn: (signal: AbortSignal) => Effect.Effect<A, E>,
|
|
61
|
-
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError> {
|
|
75
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError, RedisServiceTag> {
|
|
62
76
|
const normalizedOrgId = orgId.trim()
|
|
63
77
|
|
|
64
78
|
if (!normalizedOrgId) {
|
|
@@ -70,7 +84,8 @@ export function withOrgMemoryLockEffect<A, E>(
|
|
|
70
84
|
)
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
return Effect.
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
return Effect.gen(function* () {
|
|
88
|
+
const redis = yield* RedisServiceTag
|
|
89
|
+
return yield* withLeaseLock({ ...createOrgMemoryLockOptions(normalizedOrgId), redis: redis.getConnection() }, fn)
|
|
90
|
+
})
|
|
76
91
|
}
|
|
@@ -259,9 +259,16 @@ export function withLeaseLock<A, E, R>(
|
|
|
259
259
|
Effect.ignore,
|
|
260
260
|
)
|
|
261
261
|
|
|
262
|
+
// The refresh fiber races the user's effect against a `lockLost` Deferred
|
|
263
|
+
// so callers can observe Redis-side expirations. We pass the abort signal
|
|
264
|
+
// to `fn` so it can cooperatively wind down on lost-lock or external stop,
|
|
265
|
+
// but do NOT interrupt `fn` via `raceFirst` on `lockLost` — aborting a
|
|
266
|
+
// turn that has already produced tokens would discard the response. The
|
|
267
|
+
// caller owns the post-lease cleanup; the signal is enough for anyone who
|
|
268
|
+
// needs to bail out early.
|
|
262
269
|
return yield* Effect.scoped(
|
|
263
270
|
startRefreshFiber(redis, normalized, lockValue, abortController, lockLost).pipe(
|
|
264
|
-
Effect.andThen(fn(abortController.signal)
|
|
271
|
+
Effect.andThen(fn(abortController.signal)),
|
|
265
272
|
Effect.ensuring(cleanup),
|
|
266
273
|
),
|
|
267
274
|
)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { Context, Schema, Effect, Fiber, Layer,
|
|
1
|
+
import { Context, Schema, Effect, Fiber, Layer, Queue, Stream } from 'effect'
|
|
2
2
|
import type { Redis } from 'ioredis'
|
|
3
3
|
import type { Publisher, Subscriber } from 'resumable-stream/ioredis'
|
|
4
4
|
import { createResumableStreamContext } from 'resumable-stream/ioredis'
|
|
5
5
|
|
|
6
|
+
import { serverLogger } from '../config/logger'
|
|
6
7
|
import { RedisServiceTag } from '../effect/services'
|
|
7
8
|
import type { RedisConnectionManager } from './connection'
|
|
8
9
|
|
|
10
|
+
const SUBSCRIBER_QUEUE_CAPACITY = 1024
|
|
11
|
+
|
|
9
12
|
function toPublisher(client: Redis): Publisher {
|
|
10
13
|
return {
|
|
11
14
|
connect: () => Promise.resolve(),
|
|
@@ -26,7 +29,7 @@ class SharedSubscriberCloseError extends Schema.TaggedErrorClass<SharedSubscribe
|
|
|
26
29
|
export class SharedThreadStreamSubscriberTag extends Context.Service<
|
|
27
30
|
SharedThreadStreamSubscriberTag,
|
|
28
31
|
{ readonly subscriber: Subscriber }
|
|
29
|
-
>()('SharedThreadStreamSubscriber') {}
|
|
32
|
+
>()('@lota-sdk/core/SharedThreadStreamSubscriber') {}
|
|
30
33
|
|
|
31
34
|
export const SharedThreadStreamSubscriberLive = Layer.effect(
|
|
32
35
|
SharedThreadStreamSubscriberTag,
|
|
@@ -35,13 +38,15 @@ export const SharedThreadStreamSubscriberLive = Layer.effect(
|
|
|
35
38
|
// Disable enableReadyCheck — the ready check sends INFO which is rejected
|
|
36
39
|
// on connections in subscribe mode, causing unhandled ioredis error events.
|
|
37
40
|
const client = redisManager.getConnection().duplicate({ enableReadyCheck: false })
|
|
38
|
-
client.on('error', () => {
|
|
41
|
+
client.on('error', (error: unknown) => {
|
|
42
|
+
serverLogger.warn('Redis subscriber error', { cause: error })
|
|
43
|
+
})
|
|
39
44
|
|
|
40
|
-
const events = yield*
|
|
45
|
+
const events = yield* Queue.bounded<SharedSubscriberEvent>(SUBSCRIBER_QUEUE_CAPACITY)
|
|
41
46
|
const handlers = new Map<string, (message: string) => void>()
|
|
42
47
|
|
|
43
48
|
const dispatchFiber = yield* Effect.forkScoped(
|
|
44
|
-
Stream.
|
|
49
|
+
Stream.fromQueue(events).pipe(
|
|
45
50
|
Stream.runForEach((event) =>
|
|
46
51
|
Effect.sync(() => {
|
|
47
52
|
handlers.get(event.channel)?.(event.message)
|
|
@@ -50,10 +55,13 @@ export const SharedThreadStreamSubscriberLive = Layer.effect(
|
|
|
50
55
|
),
|
|
51
56
|
)
|
|
52
57
|
|
|
53
|
-
const currentContext = yield* Effect.context()
|
|
54
|
-
const runFork = Effect.runForkWith(currentContext)
|
|
55
58
|
const messageListener = (channel: string, message: string) => {
|
|
56
|
-
|
|
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
|
+
}
|
|
57
65
|
}
|
|
58
66
|
client.on('message', messageListener)
|
|
59
67
|
|
|
@@ -78,7 +86,7 @@ export const SharedThreadStreamSubscriberLive = Layer.effect(
|
|
|
78
86
|
handlers.clear()
|
|
79
87
|
client.removeListener('message', messageListener)
|
|
80
88
|
yield* Fiber.interrupt(dispatchFiber)
|
|
81
|
-
yield*
|
|
89
|
+
yield* Queue.shutdown(events)
|
|
82
90
|
yield* Effect.tryPromise({
|
|
83
91
|
try: () => client.quit(),
|
|
84
92
|
catch: (cause) =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ResolvedAgentConfig } from '../config/agent-defaults'
|
|
2
2
|
import { asRecord, readOptionalString } from './thread-chat-helpers'
|
|
3
3
|
|
|
4
4
|
interface RuntimeAgentIdentityOverrides {
|
|
@@ -52,11 +52,15 @@ export function readRuntimeAgentIdentityOverrides(
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function resolveRuntimeAgentDisplayName(
|
|
55
|
+
export function resolveRuntimeAgentDisplayName(
|
|
56
|
+
agentConfig: ResolvedAgentConfig,
|
|
57
|
+
overrides: RuntimeAgentIdentityOverrides,
|
|
58
|
+
agentId: string,
|
|
59
|
+
): string {
|
|
56
60
|
const override = readStringOverride(overrides.displayNamesById, agentId)
|
|
57
61
|
if (override !== undefined) {
|
|
58
62
|
return override
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
return
|
|
65
|
+
return agentConfig.displayNames[agentId] ?? agentId
|
|
62
66
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ChatMode,
|
|
2
3
|
ExecutionMode,
|
|
3
4
|
OwnershipDispatchContext,
|
|
4
5
|
PlanArtifactSubmission,
|
|
@@ -6,9 +7,8 @@ import type {
|
|
|
6
7
|
PlanNodeSpecRecord,
|
|
7
8
|
} from '@lota-sdk/shared'
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import type { ResolvedThreadBootstrapConfig } from '../config/thread-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,13 +162,18 @@ export function buildThreadAgentToolPolicy<TAgent extends string, TSkill extends
|
|
|
162
162
|
linearInstalled: boolean
|
|
163
163
|
githubInstalled: boolean
|
|
164
164
|
provideRepoTool: boolean
|
|
165
|
-
leadAgentId
|
|
165
|
+
leadAgentId: TAgent
|
|
166
166
|
onboardingOwnerAgentId?: TAgent
|
|
167
|
+
threadBootstrapConfig?: ResolvedThreadBootstrapConfig
|
|
167
168
|
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
168
169
|
}): AgentToolPolicy<TSkill> {
|
|
169
170
|
const resolvedMode = params.mode ?? toChatMode(params.threadType)
|
|
170
|
-
const leadAgentId = params.leadAgentId
|
|
171
|
-
const onboardingOwnerAgentId =
|
|
171
|
+
const leadAgentId = params.leadAgentId
|
|
172
|
+
const onboardingOwnerAgentId =
|
|
173
|
+
params.onboardingOwnerAgentId ??
|
|
174
|
+
(params.threadBootstrapConfig
|
|
175
|
+
? (resolveOnboardingOwnerAgentId(leadAgentId, params.threadBootstrapConfig) as TAgent)
|
|
176
|
+
: leadAgentId)
|
|
172
177
|
const skills = resolveActiveAgentSkills({
|
|
173
178
|
agentId: params.agentId,
|
|
174
179
|
threadType: params.threadType,
|