@lota-sdk/core 0.4.9 → 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 +2 -2
- package/src/ai/embedding-cache.ts +3 -1
- package/src/ai-gateway/ai-gateway.ts +38 -10
- package/src/config/agent-defaults.ts +22 -9
- 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 +20 -7
- package/src/config/thread-defaults.ts +12 -4
- package/src/create-runtime.ts +69 -656
- 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 +706 -538
- package/src/db/startup.ts +30 -19
- package/src/effect/awaitable-effect.ts +46 -37
- package/src/effect/helpers.ts +30 -5
- package/src/effect/index.ts +7 -5
- package/src/effect/layers.ts +82 -72
- package/src/effect/runtime.ts +18 -3
- package/src/effect/services.ts +15 -11
- package/src/embeddings/provider.ts +65 -66
- package/src/index.ts +13 -11
- package/src/queues/autonomous-job.queue.ts +59 -71
- package/src/queues/context-compaction.queue.ts +6 -18
- package/src/queues/delayed-node-promotion.queue.ts +9 -17
- package/src/queues/organization-learning.queue.ts +17 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
- package/src/queues/plan-scheduler.queue.ts +6 -18
- package/src/queues/post-chat-memory.queue.ts +6 -18
- package/src/queues/queue-factory.ts +128 -50
- package/src/queues/title-generation.queue.ts +6 -17
- package/src/redis/connection.ts +181 -164
- package/src/redis/runtime-connection.ts +13 -3
- package/src/redis/stream-context.ts +17 -9
- package/src/runtime/agent-runtime-policy.ts +1 -1
- package/src/runtime/agent-stream-helpers.ts +15 -11
- package/src/runtime/chat-run-orchestration.ts +1 -1
- package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction/context-compaction.ts +126 -82
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/graph-designer.ts +15 -7
- package/src/runtime/helper-model.ts +8 -4
- package/src/runtime/index.ts +0 -1
- 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 +33 -54
- package/src/runtime/post-turn-side-effects.ts +6 -26
- package/src/runtime/retrieval-adapters.ts +4 -4
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +3 -3
- package/src/runtime/runtime-extensions.ts +20 -9
- 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 +7 -5
- package/src/runtime/social-chat/social-chat-history.ts +21 -12
- package/src/runtime/social-chat/social-chat.ts +401 -365
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
- package/src/runtime/thread-turn-context.ts +21 -27
- package/src/services/agent-activity.service.ts +1 -1
- package/src/services/agent-executor.service.ts +179 -187
- package/src/services/artifact.service.ts +10 -5
- package/src/services/attachment.service.ts +35 -1
- package/src/services/autonomous-job.service.ts +58 -56
- 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 +1 -1
- package/src/services/document-chunk.service.ts +8 -17
- package/src/services/execution-plan/execution-plan-graph.ts +74 -52
- package/src/services/execution-plan/execution-plan.service.ts +1 -1
- package/src/services/feedback-loop.service.ts +1 -1
- package/src/services/global-orchestrator.service.ts +33 -10
- package/src/services/graph-full-routing.ts +44 -33
- 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-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 +26 -44
- 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 +132 -90
- package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
- package/src/services/plan/plan-agent-query.service.ts +1 -1
- 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 +18 -24
- package/src/services/plan/plan-coordination.service.ts +1 -1
- package/src/services/plan/plan-cycle.service.ts +171 -164
- package/src/services/plan/plan-deadline.service.ts +290 -304
- package/src/services/plan/plan-event-delivery.service.ts +44 -39
- package/src/services/plan/plan-executor-graph.ts +114 -67
- package/src/services/plan/plan-executor-helpers.ts +60 -75
- package/src/services/plan/plan-executor.service.ts +550 -467
- package/src/services/plan/plan-run.service.ts +12 -19
- package/src/services/plan/plan-scheduler.service.ts +27 -33
- 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 +17 -9
- 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 +26 -10
- 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 +24 -8
- package/src/services/thread/thread-title.service.ts +1 -1
- package/src/services/thread/thread-turn-execution.ts +1 -1
- package/src/services/thread/thread-turn-preparation.service.ts +18 -16
- package/src/services/thread/thread-turn-streaming.ts +12 -11
- package/src/services/thread/thread-turn.ts +43 -10
- package/src/services/thread/thread.service.ts +11 -2
- 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 +1 -1
- 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/title-generator.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +28 -17
- package/src/tools/fetch-webpage.tool.ts +20 -13
- package/src/tools/firecrawl-client.ts +13 -3
- package/src/tools/plan-approval.tool.ts +9 -1
- package/src/tools/search-web.tool.ts +16 -9
- package/src/tools/team-think.tool.ts +2 -2
- package/src/utils/async.ts +15 -6
- package/src/utils/errors.ts +27 -15
- package/src/workers/bootstrap.ts +25 -48
- package/src/workers/organization-learning.worker.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
- package/src/workers/worker-utils.ts +20 -2
- package/src/config/search.ts +0 -3
- 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,10 +1,20 @@
|
|
|
1
|
-
import { Effect } from 'effect'
|
|
2
1
|
import type IORedis from 'ioredis'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { resolveLotaService } from '../effect/runtime'
|
|
5
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
|
+
}
|
|
6
16
|
|
|
7
17
|
export function getRuntimeRedisConnection(): IORedis {
|
|
8
|
-
const redis =
|
|
18
|
+
const redis = currentRedisManager ?? resolveLotaService(RedisServiceTag)
|
|
9
19
|
return redis.getConnection()
|
|
10
20
|
}
|
|
@@ -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,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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
2
2
|
import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
|
|
3
|
-
import { Duration, Effect,
|
|
3
|
+
import { Duration, Effect, Exit, Scope } from 'effect'
|
|
4
4
|
|
|
5
5
|
import { getAgentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
|
|
6
6
|
import { nowEpochMillis } from '../utils/date-time'
|
|
@@ -106,17 +106,20 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
|
|
|
106
106
|
const abortFromParent = () => {
|
|
107
107
|
controller.abort((parentSignal as AbortSignal & { reason?: unknown }).reason)
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}),
|
|
118
120
|
),
|
|
119
121
|
)
|
|
122
|
+
Effect.runFork(Scope.provide(Effect.forkScoped(timeoutEffect), scope))
|
|
120
123
|
|
|
121
124
|
if (parentSignal.aborted) {
|
|
122
125
|
abortFromParent()
|
|
@@ -128,9 +131,10 @@ export function createTimedAbortSignal(parentSignal: AbortSignal, timeoutMs: num
|
|
|
128
131
|
signal: controller.signal,
|
|
129
132
|
didTimeout: () => didTimeout,
|
|
130
133
|
dispose: () => {
|
|
134
|
+
if (disposed) return
|
|
131
135
|
disposed = true
|
|
132
136
|
parentSignal.removeEventListener('abort', abortFromParent)
|
|
133
|
-
void Effect.
|
|
137
|
+
void Effect.runPromise(Scope.close(scope, Exit.void).pipe(Effect.catchCause(() => Effect.void)))
|
|
134
138
|
},
|
|
135
139
|
}
|
|
136
140
|
}
|
|
@@ -37,7 +37,7 @@ interface CompactionCoordination {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export class CompactionCoordinationTag extends Context.Service<CompactionCoordinationTag, CompactionCoordination>()(
|
|
40
|
-
'CompactionCoordination',
|
|
40
|
+
'@lota-sdk/core/CompactionCoordination',
|
|
41
41
|
) {}
|
|
42
42
|
|
|
43
43
|
export const CompactionCoordinationLive = Layer.effect(
|
|
@@ -66,7 +66,7 @@ function runContextCompacter(helperModelRuntime: HelperModelRuntime, params: Con
|
|
|
66
66
|
schema: ContextCompactionOutputSchema,
|
|
67
67
|
maxOutputTokens: 8_000,
|
|
68
68
|
}),
|
|
69
|
-
).pipe(Effect.
|
|
69
|
+
).pipe(Effect.flatMap(parseCompactionOutput)),
|
|
70
70
|
)
|
|
71
71
|
}
|
|
72
72
|
|