@lota-sdk/core 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +4 -4
  2. package/src/ai/embedding-cache.ts +17 -11
  3. package/src/ai-gateway/ai-gateway.ts +164 -94
  4. package/src/ai-gateway/index.ts +4 -1
  5. package/src/config/agent-defaults.ts +2 -2
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/create-runtime.ts +259 -200
  8. package/src/db/cursor-pagination.ts +2 -9
  9. package/src/db/memory-store.ts +194 -175
  10. package/src/db/memory.ts +125 -71
  11. package/src/db/schema-fingerprint.ts +5 -4
  12. package/src/db/service-normalization.ts +4 -3
  13. package/src/db/service.ts +3 -2
  14. package/src/db/startup.ts +15 -16
  15. package/src/effect/errors.ts +161 -21
  16. package/src/effect/index.ts +0 -1
  17. package/src/embeddings/provider.ts +15 -7
  18. package/src/queues/autonomous-job.queue.ts +10 -22
  19. package/src/queues/delayed-node-promotion.queue.ts +8 -14
  20. package/src/queues/document-processor.queue.ts +13 -4
  21. package/src/queues/memory-consolidation.queue.ts +26 -14
  22. package/src/queues/plan-agent-heartbeat.queue.ts +10 -9
  23. package/src/queues/plan-scheduler.queue.ts +37 -15
  24. package/src/queues/queue-factory.ts +59 -35
  25. package/src/queues/standalone-worker.ts +3 -2
  26. package/src/redis/connection.ts +10 -3
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +5 -5
  29. package/src/redis/stream-context.ts +1 -1
  30. package/src/runtime/chat-message.ts +64 -1
  31. package/src/runtime/chat-run-orchestration.ts +33 -20
  32. package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
  33. package/src/runtime/context-compaction/context-compaction.ts +78 -66
  34. package/src/runtime/domain-layer.ts +13 -7
  35. package/src/runtime/execution-plan.ts +7 -3
  36. package/src/runtime/live-turn-trace.ts +6 -49
  37. package/src/runtime/memory/memory-block.ts +3 -9
  38. package/src/runtime/memory/memory-scope.ts +3 -1
  39. package/src/runtime/plugin-resolution.ts +2 -1
  40. package/src/runtime/post-turn-side-effects.ts +6 -5
  41. package/src/runtime/retrieval-adapters.ts +8 -20
  42. package/src/runtime/runtime-config.ts +3 -9
  43. package/src/runtime/runtime-extensions.ts +2 -4
  44. package/src/runtime/runtime-lifecycle.ts +56 -16
  45. package/src/runtime/runtime-services.ts +180 -102
  46. package/src/runtime/runtime-worker-registry.ts +3 -1
  47. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  48. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  49. package/src/runtime/social-chat/social-chat.ts +356 -223
  50. package/src/runtime/specialist-runner.ts +3 -1
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  52. package/src/runtime/thread-turn-context.ts +142 -102
  53. package/src/runtime/turn-lifecycle.ts +15 -46
  54. package/src/services/agent-activity.service.ts +1 -1
  55. package/src/services/agent-executor.service.ts +107 -77
  56. package/src/services/autonomous-job.service.ts +354 -293
  57. package/src/services/background-work.service.ts +3 -3
  58. package/src/services/context-compaction.service.ts +7 -2
  59. package/src/services/document-chunk.service.ts +50 -32
  60. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  61. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  62. package/src/services/feedback-loop.service.ts +5 -4
  63. package/src/services/graph-full-routing.ts +37 -36
  64. package/src/services/institutional-memory.service.ts +28 -30
  65. package/src/services/learned-skill.service.ts +107 -72
  66. package/src/services/memory/memory-errors.ts +4 -23
  67. package/src/services/memory/memory-org-memory.ts +10 -5
  68. package/src/services/memory/memory-rerank.ts +18 -6
  69. package/src/services/memory/memory.service.ts +170 -111
  70. package/src/services/memory/rerank.service.ts +29 -20
  71. package/src/services/organization-member.service.ts +1 -1
  72. package/src/services/organization.service.ts +69 -75
  73. package/src/services/ownership-dispatcher.service.ts +40 -39
  74. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  75. package/src/services/plan/plan-agent-query.service.ts +39 -31
  76. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  77. package/src/services/plan/plan-coordination.service.ts +2 -1
  78. package/src/services/plan/plan-cycle.service.ts +6 -5
  79. package/src/services/plan/plan-deadline.service.ts +57 -54
  80. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  81. package/src/services/plan/plan-executor-graph.ts +18 -15
  82. package/src/services/plan/plan-executor.service.ts +235 -262
  83. package/src/services/plan/plan-run.service.ts +169 -93
  84. package/src/services/plan/plan-scheduler.service.ts +192 -202
  85. package/src/services/plan/plan-template.service.ts +1 -1
  86. package/src/services/plan/plan-transaction-events.ts +1 -1
  87. package/src/services/plan/plan-workspace.service.ts +23 -14
  88. package/src/services/plugin-executor.service.ts +5 -9
  89. package/src/services/queue-job.service.ts +117 -59
  90. package/src/services/recent-activity-title.service.ts +13 -12
  91. package/src/services/recent-activity.service.ts +6 -1
  92. package/src/services/social-chat-history.service.ts +29 -25
  93. package/src/services/system-executor.service.ts +5 -9
  94. package/src/services/thread/thread-active-run.ts +2 -2
  95. package/src/services/thread/thread-listing.ts +61 -57
  96. package/src/services/thread/thread-memory-block.ts +73 -48
  97. package/src/services/thread/thread-message.service.ts +76 -65
  98. package/src/services/thread/thread-record-store.ts +8 -8
  99. package/src/services/thread/thread-title.service.ts +10 -4
  100. package/src/services/thread/thread-turn-execution.ts +43 -45
  101. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  102. package/src/services/thread/thread-turn-streaming.ts +82 -85
  103. package/src/services/thread/thread-turn.ts +8 -8
  104. package/src/services/thread/thread.service.ts +135 -100
  105. package/src/services/user.service.ts +45 -48
  106. package/src/storage/attachment-parser.ts +6 -2
  107. package/src/storage/attachment-storage.service.ts +5 -6
  108. package/src/storage/generated-document-storage.service.ts +1 -1
  109. package/src/system-agents/context-compaction.agent.ts +10 -9
  110. package/src/system-agents/delegated-agent-factory.ts +30 -6
  111. package/src/system-agents/memory-reranker.agent.ts +10 -9
  112. package/src/system-agents/memory.agent.ts +10 -9
  113. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  114. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  115. package/src/system-agents/skill-extractor.agent.ts +13 -12
  116. package/src/system-agents/skill-manager.agent.ts +13 -12
  117. package/src/system-agents/thread-router.agent.ts +10 -5
  118. package/src/system-agents/title-generator.agent.ts +13 -12
  119. package/src/tools/fetch-webpage.tool.ts +13 -13
  120. package/src/tools/memory-block.tool.ts +3 -1
  121. package/src/tools/plan-approval.tool.ts +4 -2
  122. package/src/tools/read-file-parts.tool.ts +10 -4
  123. package/src/tools/remember-memory.tool.ts +3 -1
  124. package/src/tools/research-topic.tool.ts +9 -5
  125. package/src/tools/search-web.tool.ts +16 -16
  126. package/src/tools/search.tool.ts +20 -5
  127. package/src/tools/team-think.tool.ts +61 -38
  128. package/src/utils/async.ts +5 -5
  129. package/src/utils/errors.ts +19 -18
  130. package/src/utils/sse-keepalive.ts +28 -25
  131. package/src/workers/bootstrap.ts +75 -11
  132. package/src/workers/memory-consolidation.worker.ts +82 -91
  133. package/src/workers/organization-learning.worker.ts +14 -4
  134. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  135. package/src/workers/skill-extraction.runner.ts +97 -61
  136. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  137. package/src/workers/utils/thread-message-query.ts +24 -24
  138. package/src/workers/worker-utils.ts +23 -4
  139. package/src/effect/helpers.ts +0 -123
@@ -1,8 +1,15 @@
1
1
  import type { Fiber } from 'effect'
2
- import { Effect, ManagedRuntime } from 'effect'
2
+ import { Deferred, Effect, Layer, ManagedRuntime } from 'effect'
3
3
  import type { Subscriber } from 'resumable-stream/ioredis'
4
4
 
5
- import { AiGatewayTag, bindAiGatewayRuntime } from './ai-gateway/ai-gateway'
5
+ import {
6
+ AiGatewayModelsTag,
7
+ AiGatewayTag,
8
+ RuntimeBridgeTag,
9
+ createAiGatewayModels,
10
+ makeAiGatewayService,
11
+ } from './ai-gateway/ai-gateway'
12
+ import type { AiGatewayModels, RuntimeBridge } from './ai-gateway/ai-gateway'
6
13
  import { computeSchemaFingerprint } from './db/schema-fingerprint'
7
14
  import { publishDatabaseBootstrapEffect } from './db/startup'
8
15
  import {
@@ -12,10 +19,8 @@ import {
12
19
  DatabaseServiceTag as EffectDatabaseService,
13
20
  RedisServiceTag as EffectRedisService,
14
21
  RuntimeAdaptersServiceTag,
15
- RuntimeConfigServiceTag,
16
22
  } from './effect'
17
23
  import { ConfigurationError, ServiceError } from './effect/errors'
18
- import { effectTryPromise } from './effect/helpers'
19
24
  import { LotaQueuesServiceTag } from './queues/queues.service'
20
25
  import type { RedisConnectionManager } from './redis/connection'
21
26
  import { SharedThreadStreamSubscriberTag } from './redis/stream-context'
@@ -74,6 +79,10 @@ export interface LotaRuntime {
74
79
  config: ResolvedLotaRuntimeConfig
75
80
  plugins: Record<string, LotaPlugin>
76
81
  systemExecutors: Record<string, SystemNodeExecutor>
82
+ /** Pre-bound AI gateway model factories. Use instead of the legacy
83
+ * `aiGatewayChatModel(modelId)` / `aiGatewayModel(modelId)` module-level
84
+ * helpers — those now require a `deps` argument. */
85
+ ai: AiGatewayModels
77
86
  connectPluginDatabases(): Promise<void>
78
87
  connect(): Promise<void>
79
88
  disconnect(): Promise<void>
@@ -88,87 +97,136 @@ export function createLotaRuntimeFromEnv(
88
97
  )
89
98
  }
90
99
 
91
- export function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntime> {
92
- let effectRuntime: { dispose(): Promise<void> } | null = null
100
+ // @effect-diagnostics-next-line asyncFunction:off -- public host entrypoint; returns Promise by design.
101
+ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntime> {
102
+ const resolvedConfig = parseLotaRuntimeConfig(config)
103
+ const systemExecutors = { ...getBuiltInSystemExecutors(), ...resolvedConfig.systemExecutors }
104
+ const runtimeConfig = { ...resolvedConfig, systemExecutors } satisfies ResolvedLotaRuntimeConfig
93
105
 
94
- return Effect.runPromise(
95
- Effect.gen(function* () {
96
- const resolvedConfig = parseLotaRuntimeConfig(config)
97
- const systemExecutors = { ...getBuiltInSystemExecutors(), ...resolvedConfig.systemExecutors }
98
- const runtimeConfig = { ...resolvedConfig, systemExecutors } satisfies ResolvedLotaRuntimeConfig
106
+ const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
107
+ const socialChatAgentDisplayName = runtimeConfig.socialChat?.agentDisplayName?.trim() || 'Lota'
108
+ if (runtimeConfig.socialChat && !runtimeConfig.agents.roster.includes(socialChatAgentId)) {
109
+ throw new ConfigurationError({
110
+ message: `socialChat.agentId must be present in agents.roster: ${socialChatAgentId}`,
111
+ key: 'socialChat.agentId',
112
+ })
113
+ }
114
+ const resolvedAgentDisplayNames = runtimeConfig.socialChat
115
+ ? { ...runtimeConfig.agents.displayNames, [socialChatAgentId]: socialChatAgentDisplayName }
116
+ : runtimeConfig.agents.displayNames
99
117
 
100
- const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
101
- const socialChatAgentDisplayName = runtimeConfig.socialChat?.agentDisplayName?.trim() || 'Lota'
102
- if (runtimeConfig.socialChat && !runtimeConfig.agents.roster.includes(socialChatAgentId)) {
103
- return yield* new ConfigurationError({
104
- message: `socialChat.agentId must be present in agents.roster: ${socialChatAgentId}`,
105
- key: 'socialChat.agentId',
106
- })
107
- }
108
- const resolvedAgentDisplayNames = runtimeConfig.socialChat
109
- ? { ...runtimeConfig.agents.displayNames, [socialChatAgentId]: socialChatAgentDisplayName }
110
- : runtimeConfig.agents.displayNames
118
+ const infrastructureLayer = buildInfrastructureLayer(runtimeConfig, { resolvedAgentDisplayNames })
119
+ const aiGateway = Effect.runSync(makeAiGatewayService(runtimeConfig))
120
+ const aiGatewayModelsDeferred = Effect.runSync(Deferred.make<AiGatewayModels>())
121
+ const runtimeBridgeDeferred = Effect.runSync(Deferred.make<RuntimeBridge>())
122
+ const bridgeLayer = Layer.mergeAll(
123
+ Layer.succeed(AiGatewayTag, aiGateway),
124
+ Layer.effect(AiGatewayModelsTag, Deferred.await(aiGatewayModelsDeferred)),
125
+ Layer.effect(RuntimeBridgeTag, Deferred.await(runtimeBridgeDeferred)),
126
+ )
127
+ const fullLayer = buildDomainServiceLayer(infrastructureLayer, bridgeLayer)
128
+ const managedRuntime = ManagedRuntime.make(fullLayer)
129
+ const runtimeBridge: RuntimeBridge = {
130
+ runPromise: (effect, options) => managedRuntime.runPromise(effect, options),
131
+ runFork: (effect) => managedRuntime.runFork(effect),
132
+ }
133
+ const aiGatewayModels = createAiGatewayModels({
134
+ gateway: aiGateway,
135
+ runtimeConfig,
136
+ runPromise: runtimeBridge.runPromise,
137
+ runFork: runtimeBridge.runFork,
138
+ })
111
139
 
112
- // ── Infrastructure + domain layer composition ─────────────────────
113
- const infrastructureLayer = buildInfrastructureLayer(runtimeConfig, { resolvedAgentDisplayNames })
114
- const fullLayer = buildDomainServiceLayer(infrastructureLayer)
140
+ if (!Effect.runSync(Deferred.succeed(runtimeBridgeDeferred, runtimeBridge))) {
141
+ throw new ServiceError({ message: 'Failed to initialize the runtime bridge.' })
142
+ }
143
+ if (!Effect.runSync(Deferred.succeed(aiGatewayModelsDeferred, aiGatewayModels))) {
144
+ throw new ServiceError({ message: 'Failed to initialize AI gateway models.' })
145
+ }
115
146
 
116
- const managedRuntime = ManagedRuntime.make(fullLayer)
117
- effectRuntime = managedRuntime
147
+ try {
148
+ const resolvedServices = await managedRuntime.runPromise(
149
+ Effect.gen(function* () {
150
+ return {
151
+ db: yield* EffectDatabaseService,
152
+ redisManager: yield* EffectRedisService,
153
+ sharedSubscriber: yield* SharedThreadStreamSubscriberTag,
154
+ threadTurnService: yield* ThreadTurnServiceTag,
155
+ socialChatHistoryService: yield* SocialChatHistoryServiceTag,
156
+ learnedSkillService: yield* LearnedSkillServiceTag,
157
+ memoryService: yield* MemoryServiceTag,
158
+ runtimeAdapters: yield* RuntimeAdaptersServiceTag,
159
+ agentConfig: yield* AgentConfigServiceTag,
160
+ agentFactoryConfig: yield* AgentFactoryServiceTag,
161
+ queues: yield* LotaQueuesServiceTag,
162
+ autonomousJobService: yield* AutonomousJobServiceTag,
163
+ contextCompactionService: yield* ContextCompactionServiceTag,
164
+ threadService: yield* ThreadServiceTag,
165
+ planAgentHeartbeatService: yield* PlanAgentHeartbeatServiceTag,
166
+ planSchedulerService: yield* PlanSchedulerServiceTag,
167
+ planDeadlineService: yield* PlanDeadlineServiceTag,
168
+ planExecutorService: yield* PlanExecutorServiceTag,
169
+ planCycleService: yield* PlanCycleServiceTag,
170
+ threadTitleService: yield* ThreadTitleServiceTag,
171
+ recentActivityTitleService: yield* RecentActivityTitleServiceTag,
172
+ }
173
+ }),
174
+ )
175
+ const {
176
+ db,
177
+ redisManager,
178
+ sharedSubscriber,
179
+ threadTurnService,
180
+ socialChatHistoryService,
181
+ learnedSkillService,
182
+ memoryService,
183
+ runtimeAdapters,
184
+ agentConfig,
185
+ agentFactoryConfig,
186
+ queues,
187
+ autonomousJobService,
188
+ contextCompactionService,
189
+ threadService,
190
+ planAgentHeartbeatService,
191
+ planSchedulerService,
192
+ planDeadlineService,
193
+ planExecutorService,
194
+ planCycleService,
195
+ threadTitleService,
196
+ recentActivityTitleService,
197
+ } = resolvedServices
118
198
 
119
- // Eagerly resolve the services the outer entrypoint needs directly.
120
- const resolvedServices = yield* Effect.tryPromise({
121
- try: () =>
122
- managedRuntime.runPromise(
123
- Effect.gen(function* () {
124
- return {
125
- db: yield* EffectDatabaseService,
126
- redisManager: yield* EffectRedisService,
127
- sharedSubscriber: yield* SharedThreadStreamSubscriberTag,
128
- threadTurnService: yield* ThreadTurnServiceTag,
129
- socialChatHistoryService: yield* SocialChatHistoryServiceTag,
130
- learnedSkillService: yield* LearnedSkillServiceTag,
131
- memoryService: yield* MemoryServiceTag,
132
- runtimeAdapters: yield* RuntimeAdaptersServiceTag,
133
- agentConfig: yield* AgentConfigServiceTag,
134
- agentFactoryConfig: yield* AgentFactoryServiceTag,
135
- queues: yield* LotaQueuesServiceTag,
136
- autonomousJobService: yield* AutonomousJobServiceTag,
137
- contextCompactionService: yield* ContextCompactionServiceTag,
138
- threadService: yield* ThreadServiceTag,
139
- planAgentHeartbeatService: yield* PlanAgentHeartbeatServiceTag,
140
- planSchedulerService: yield* PlanSchedulerServiceTag,
141
- planDeadlineService: yield* PlanDeadlineServiceTag,
142
- planExecutorService: yield* PlanExecutorServiceTag,
143
- planCycleService: yield* PlanCycleServiceTag,
144
- threadTitleService: yield* ThreadTitleServiceTag,
145
- recentActivityTitleService: yield* RecentActivityTitleServiceTag,
146
- aiGateway: yield* AiGatewayTag,
147
- runtimeConfigService: yield* RuntimeConfigServiceTag,
148
- }
149
- }),
150
- ),
151
- catch: (error) =>
152
- new ServiceError({
153
- message: `Failed to initialize Effect runtime services: ${error instanceof Error ? error.message : String(error)}`,
154
- cause: error,
155
- }),
156
- })
157
- const {
158
- db,
159
- redisManager,
160
- sharedSubscriber,
161
- threadTurnService,
162
- socialChatHistoryService,
163
- learnedSkillService,
164
- memoryService,
165
- runtimeAdapters,
166
- agentConfig,
167
- agentFactoryConfig,
168
- queues,
169
- autonomousJobService,
170
- contextCompactionService,
199
+ // ── Schema + plugin + worker + social-chat composition ────────────
200
+ const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
201
+ const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
202
+ const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
203
+ const schemaFiles = [
204
+ ...getBuiltInSchemaFiles(),
205
+ ...(runtimeConfig.extraSchemaFiles ?? []),
206
+ ...hostContributionSchemaFiles,
207
+ ]
208
+ const contributionEnvKeys = [...LOTA_RUNTIME_ENV_KEYS, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
209
+ const connectedPluginDatabases = new Set<string>()
210
+ const connectPluginDatabases = createPluginDatabaseConnector(
211
+ managedRuntime,
212
+ pluginRuntime,
213
+ connectedPluginDatabases,
214
+ )
215
+ const disconnectPluginDatabases = createPluginDatabaseDisconnector(
216
+ managedRuntime,
217
+ pluginRuntime,
218
+ connectedPluginDatabases,
219
+ )
220
+ const workers = buildRuntimeWorkerRegistry(
221
+ queues,
222
+ {
223
+ databaseService: db,
224
+ runPromise: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
225
+ managedRuntime.runPromise(effect as unknown as Effect.Effect<A, E, never>),
171
226
  threadService,
227
+ contextCompactionService,
228
+ autonomousJobService,
229
+ memoryService,
172
230
  planAgentHeartbeatService,
173
231
  planSchedulerService,
174
232
  planDeadlineService,
@@ -176,134 +234,135 @@ export function createLotaRuntime(config: LotaRuntimeConfig): Promise<LotaRuntim
176
234
  planCycleService,
177
235
  threadTitleService,
178
236
  recentActivityTitleService,
179
- aiGateway,
180
- runtimeConfigService,
181
- } = resolvedServices
182
-
183
- // Seed the AI gateway middleware so wrapGenerate/wrapStream/wrapEmbed
184
- // edges can run Effects without reaching for an ambient runtime slot.
185
- bindAiGatewayRuntime({
186
- gateway: aiGateway,
187
- runtimeConfig: runtimeConfigService,
188
- runPromise: (effect) => managedRuntime.runPromise(effect),
189
- runFork: (effect) => managedRuntime.runFork(effect),
190
- })
191
-
192
- // ── Schema + plugin + worker + social-chat composition ────────────
193
- const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
194
- const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
195
- const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
196
- const schemaFiles = [
197
- ...getBuiltInSchemaFiles(),
198
- ...(runtimeConfig.extraSchemaFiles ?? []),
199
- ...hostContributionSchemaFiles,
200
- ]
201
- const contributionEnvKeys = [...LOTA_RUNTIME_ENV_KEYS, ...pluginContributions.flatMap((plugin) => plugin.envKeys)]
202
- const connectedPluginDatabases = new Set<string>()
203
- const connectPluginDatabases = createPluginDatabaseConnector(
204
- managedRuntime,
205
- pluginRuntime,
206
- connectedPluginDatabases,
207
- )
208
- const disconnectPluginDatabases = createPluginDatabaseDisconnector(
209
- managedRuntime,
210
- pluginRuntime,
211
- connectedPluginDatabases,
212
- )
213
- const workers = buildRuntimeWorkerRegistry(
214
- queues,
215
- {
216
- databaseService: db,
217
- threadService,
218
- contextCompactionService,
219
- autonomousJobService,
220
- memoryService,
221
- planAgentHeartbeatService,
222
- planSchedulerService,
223
- planDeadlineService,
224
- planExecutorService,
225
- planCycleService,
226
- threadTitleService,
227
- recentActivityTitleService,
228
- redisManager,
229
- },
230
- runtimeConfig.extraWorkers,
231
- )
232
- const socialChat = createSocialChatRuntime({
233
- agentConfig,
234
- agentFactoryConfig,
235
- redisClient: redisManager.getConnection() as unknown as Parameters<
236
- typeof createSocialChatRuntime
237
- >[0]['redisClient'],
238
- socialChat: runtimeConfig.socialChat,
239
- services: { learnedSkillService, memoryService, socialChatHistoryService, runtimeAdapters, queues },
240
- })
241
- const currentContext = yield* Effect.context()
242
- const runPromiseWithCurrentContext = Effect.runPromiseWith(currentContext)
243
-
244
- // ── Service surface (eager, plain-property) ───────────────────────
245
- const { services, lota } = buildRuntimeServiceSurface({
246
- managedRuntime,
247
- db,
248
237
  redisManager,
249
- sharedSubscriber,
250
- threadTurnService,
251
- socialChatHistoryService,
252
- })
238
+ },
239
+ runtimeConfig.extraWorkers,
240
+ )
241
+ const socialChat = createSocialChatRuntime({
242
+ agentConfig,
243
+ agentFactoryConfig,
244
+ // Bridge ioredis version mismatch: `@chat-adapter/state-ioredis` ships
245
+ // its own Redis type with newer methods (hexpireat etc.) not present
246
+ // in our pinned ioredis. The runtime behaviour is compatible.
247
+ redisClient: redisManager.getConnection() as unknown as Parameters<
248
+ typeof createSocialChatRuntime
249
+ >[0]['redisClient'],
250
+ socialChat: runtimeConfig.socialChat,
251
+ services: { learnedSkillService, memoryService, socialChatHistoryService, runtimeAdapters, queues },
252
+ })
253
253
 
254
- const disconnect = createRuntimeDisconnect({
255
- managedRuntime,
256
- runPromiseWithCurrentContext,
257
- socialChatShutdown: () => socialChat.shutdown(),
258
- disconnectPluginDatabases,
259
- })
254
+ // ── Service surface (eager, plain-property) ───────────────────────
255
+ const { services, lota } = buildRuntimeServiceSurface({
256
+ managedRuntime,
257
+ db,
258
+ redisManager,
259
+ sharedSubscriber,
260
+ threadTurnService,
261
+ socialChatHistoryService,
262
+ })
260
263
 
261
- const lotaRuntime: LotaRuntime = {
262
- runPromise: <A, E, R>(effect: Effect.Effect<A, E, R>, options?: { readonly signal?: AbortSignal }) =>
263
- managedRuntime.runPromise(effect as unknown as Effect.Effect<A, E, never>, options),
264
- runSync: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
265
- managedRuntime.runSync(effect as unknown as Effect.Effect<A, E, never>),
266
- runFork: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
267
- managedRuntime.runFork(effect as unknown as Effect.Effect<A, E, never>),
268
- services,
269
- lota,
270
- redis: {
271
- manager: redisManager,
272
- subscriber: sharedSubscriber.subscriber,
273
- getConnection: () => redisManager.getConnection(),
274
- getConnectionForBullMQ: () => redisManager.getConnectionForBullMQ(),
275
- closeConnection: () => managedRuntime.runPromise(effectTryPromise(() => redisManager.closeConnection())),
276
- },
277
- workers,
278
- socialChat,
279
- schemaFiles,
280
- contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
281
- config: runtimeConfig,
282
- plugins: pluginRuntime,
283
- systemExecutors,
284
- connectPluginDatabases,
285
- connect: () =>
264
+ const disconnect = createRuntimeDisconnect({
265
+ managedRuntime,
266
+ socialChatShutdown: () => socialChat.shutdown(),
267
+ disconnectPluginDatabases,
268
+ })
269
+
270
+ // Host boundary: the ManagedRuntime carries the full 76-service union
271
+ // the SDK composes, but exposing that union in the public `LotaRuntime`
272
+ // interface would force every consumer to import every service tag. The
273
+ // cast keeps the public signature generic while trusting the runtime's
274
+ // actual capabilities — any missing service surfaces at runtime.
275
+ const lotaRuntime: LotaRuntime = {
276
+ runPromise: <A, E, R>(effect: Effect.Effect<A, E, R>, options?: { readonly signal?: AbortSignal }) =>
277
+ managedRuntime.runPromise(effect as unknown as Effect.Effect<A, E, never>, options),
278
+ runSync: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
279
+ managedRuntime.runSync(effect as unknown as Effect.Effect<A, E, never>),
280
+ runFork: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
281
+ managedRuntime.runFork(effect as unknown as Effect.Effect<A, E, never>),
282
+ services,
283
+ lota,
284
+ redis: {
285
+ manager: redisManager,
286
+ subscriber: sharedSubscriber.subscriber,
287
+ getConnection: () => redisManager.getConnection(),
288
+ getConnectionForBullMQ: () => redisManager.getConnectionForBullMQ(),
289
+ closeConnection: () =>
286
290
  managedRuntime.runPromise(
287
- Effect.gen(function* () {
288
- yield* db.connect()
289
- const bunFiles = schemaFiles.map((schemaFile) =>
290
- schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
291
- )
292
- yield* db.applySchema(bunFiles)
293
- const schemaFingerprint = yield* effectTryPromise(() => computeSchemaFingerprint(schemaFiles))
294
- yield* publishDatabaseBootstrapEffect({ databaseService: db, schemaFingerprint })
291
+ Effect.tryPromise({
292
+ try: () => redisManager.closeConnection(),
293
+ catch: (cause) => new ServiceError({ message: 'Failed to close Redis connection.', cause }),
295
294
  }),
296
295
  ),
297
- disconnect,
298
- }
299
- return lotaRuntime
300
- }),
301
- ).catch((error) => {
302
- if (effectRuntime) {
303
- void effectRuntime.dispose().catch(() => undefined)
296
+ },
297
+ workers,
298
+ socialChat,
299
+ ai: aiGatewayModels,
300
+ schemaFiles,
301
+ contributions: { envKeys: [...new Set(contributionEnvKeys)], schemaFiles: hostContributionSchemaFiles },
302
+ config: runtimeConfig,
303
+ plugins: pluginRuntime,
304
+ systemExecutors,
305
+ connectPluginDatabases,
306
+ connect: () =>
307
+ managedRuntime.runPromise(
308
+ Effect.gen(function* () {
309
+ yield* db.connect()
310
+ const bunFiles = schemaFiles.map((schemaFile) =>
311
+ schemaFile instanceof URL ? Bun.file(schemaFile.pathname) : Bun.file(schemaFile),
312
+ )
313
+ yield* db.applySchema(bunFiles)
314
+ const schemaFingerprint = yield* Effect.tryPromise({
315
+ try: () => computeSchemaFingerprint(schemaFiles),
316
+ catch: (cause) => new ServiceError({ message: 'Failed to compute schema fingerprint.', cause }),
317
+ })
318
+ yield* publishDatabaseBootstrapEffect({ databaseService: db, schemaFingerprint })
319
+ }),
320
+ ),
321
+ disconnect,
304
322
  }
305
- throw error
306
- })
323
+ registerRuntimeShutdownHandlers(lotaRuntime)
324
+ return lotaRuntime
325
+ } catch (error) {
326
+ await managedRuntime.dispose()
327
+ throw new ServiceError({
328
+ message: `Failed to initialize Effect runtime services: ${error instanceof Error ? error.message : String(error)}`,
329
+ cause: error,
330
+ })
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Per-runtime shutdown handler — the previous module-level latch meant that
336
+ * a second `createLotaRuntime` call (e.g. reconnect after a test harness
337
+ * disposes the first runtime) silently skipped signal wiring and left the
338
+ * fresh runtime leaking on SIGTERM/SIGINT. We instead maintain a live set
339
+ * and dispatch each signal to every registered runtime, removing each as it
340
+ * disconnects. `process.on` (not `once`) keeps the dispatcher alive across
341
+ * multiple registrations within the same process.
342
+ */
343
+ const activeShutdownHandlers = new Set<() => Promise<void>>()
344
+ let signalDispatcherInstalled = false
345
+
346
+ function installSignalDispatcherOnce(): void {
347
+ if (signalDispatcherInstalled) return
348
+ signalDispatcherInstalled = true
349
+ const dispatch = () => {
350
+ for (const handler of activeShutdownHandlers) {
351
+ void handler().catch(() => undefined)
352
+ }
353
+ }
354
+ process.on('SIGTERM', dispatch)
355
+ process.on('SIGINT', dispatch)
356
+ }
357
+
358
+ function registerRuntimeShutdownHandlers(runtime: LotaRuntime): void {
359
+ installSignalDispatcherOnce()
360
+ // @effect-diagnostics-next-line asyncFunction:off -- signal dispatcher callback; Promise-based by design.
361
+ const handler = async () => {
362
+ activeShutdownHandlers.delete(handler)
363
+ await runtime.disconnect().catch(() => undefined)
364
+ }
365
+ activeShutdownHandlers.add(handler)
307
366
  }
308
367
 
309
368
  function getBuiltInSchemaFiles(): URL[] {
@@ -3,6 +3,7 @@ import { Schema, Effect } from 'effect'
3
3
  import type { BoundQuery, RecordId } from 'surrealdb'
4
4
  import { z } from 'zod'
5
5
 
6
+ import { ERROR_TAGS } from '../effect/errors'
6
7
  import type { RecordIdRef } from './record-id'
7
8
  import type { SurrealDBService } from './service'
8
9
  import type { DatabaseTable } from './tables'
@@ -25,19 +26,11 @@ export interface CursorPaginationConfig {
25
26
  queryBefore: (parentId: RecordIdRef, cursorCreatedAt: Date, cursorId: RecordId, limit: number) => BoundQuery
26
27
  }
27
28
 
28
- class CursorPaginationError extends Schema.TaggedErrorClass<CursorPaginationError>()('CursorPaginationError', {
29
+ class CursorPaginationError extends Schema.TaggedErrorClass<CursorPaginationError>()(ERROR_TAGS.CursorPaginationError, {
29
30
  message: Schema.String,
30
31
  cause: Schema.optional(Schema.Defect),
31
32
  }) {}
32
33
 
33
- export function listMessageHistoryPage(
34
- db: SurrealDBService,
35
- config: CursorPaginationConfig,
36
- params: { parentId: RecordIdRef; take: number; beforeMessageId?: string },
37
- ): Promise<MessageHistoryPage> {
38
- return Effect.runPromise(listMessageHistoryPageEffect(db, config, params))
39
- }
40
-
41
34
  export function listMessageHistoryPageEffect(
42
35
  db: SurrealDBService,
43
36
  config: CursorPaginationConfig,