@lota-sdk/core 0.4.13 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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/memory/memory-block.ts +3 -9
  37. package/src/runtime/memory/memory-scope.ts +3 -1
  38. package/src/runtime/plugin-resolution.ts +2 -1
  39. package/src/runtime/post-turn-side-effects.ts +6 -5
  40. package/src/runtime/retrieval-adapters.ts +8 -20
  41. package/src/runtime/runtime-config.ts +3 -9
  42. package/src/runtime/runtime-extensions.ts +2 -4
  43. package/src/runtime/runtime-lifecycle.ts +56 -16
  44. package/src/runtime/runtime-services.ts +180 -102
  45. package/src/runtime/runtime-worker-registry.ts +3 -1
  46. package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
  47. package/src/runtime/social-chat/social-chat-history.ts +21 -18
  48. package/src/runtime/social-chat/social-chat.ts +356 -223
  49. package/src/runtime/specialist-runner.ts +3 -1
  50. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
  51. package/src/runtime/thread-turn-context.ts +142 -102
  52. package/src/runtime/turn-lifecycle.ts +15 -46
  53. package/src/services/agent-activity.service.ts +1 -1
  54. package/src/services/agent-executor.service.ts +107 -77
  55. package/src/services/autonomous-job.service.ts +354 -293
  56. package/src/services/background-work.service.ts +3 -3
  57. package/src/services/context-compaction.service.ts +7 -2
  58. package/src/services/document-chunk.service.ts +50 -32
  59. package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
  60. package/src/services/execution-plan/execution-plan.service.ts +162 -179
  61. package/src/services/feedback-loop.service.ts +5 -4
  62. package/src/services/graph-full-routing.ts +37 -36
  63. package/src/services/institutional-memory.service.ts +28 -30
  64. package/src/services/learned-skill.service.ts +107 -72
  65. package/src/services/memory/memory-errors.ts +4 -23
  66. package/src/services/memory/memory-org-memory.ts +10 -5
  67. package/src/services/memory/memory-rerank.ts +18 -6
  68. package/src/services/memory/memory.service.ts +170 -111
  69. package/src/services/memory/rerank.service.ts +29 -20
  70. package/src/services/organization-member.service.ts +1 -1
  71. package/src/services/organization.service.ts +69 -75
  72. package/src/services/ownership-dispatcher.service.ts +40 -39
  73. package/src/services/plan/plan-agent-heartbeat.service.ts +26 -23
  74. package/src/services/plan/plan-agent-query.service.ts +39 -31
  75. package/src/services/plan/plan-completion-side-effects.ts +13 -17
  76. package/src/services/plan/plan-coordination.service.ts +2 -1
  77. package/src/services/plan/plan-cycle.service.ts +6 -5
  78. package/src/services/plan/plan-deadline.service.ts +57 -54
  79. package/src/services/plan/plan-event-delivery.service.ts +5 -4
  80. package/src/services/plan/plan-executor-graph.ts +18 -15
  81. package/src/services/plan/plan-executor.service.ts +235 -262
  82. package/src/services/plan/plan-run.service.ts +169 -93
  83. package/src/services/plan/plan-scheduler.service.ts +192 -202
  84. package/src/services/plan/plan-template.service.ts +1 -1
  85. package/src/services/plan/plan-transaction-events.ts +1 -1
  86. package/src/services/plan/plan-workspace.service.ts +23 -14
  87. package/src/services/plugin-executor.service.ts +5 -9
  88. package/src/services/queue-job.service.ts +117 -59
  89. package/src/services/recent-activity-title.service.ts +13 -12
  90. package/src/services/recent-activity.service.ts +6 -1
  91. package/src/services/social-chat-history.service.ts +29 -25
  92. package/src/services/system-executor.service.ts +5 -9
  93. package/src/services/thread/thread-active-run.ts +2 -2
  94. package/src/services/thread/thread-listing.ts +61 -57
  95. package/src/services/thread/thread-memory-block.ts +73 -48
  96. package/src/services/thread/thread-message.service.ts +76 -65
  97. package/src/services/thread/thread-record-store.ts +8 -8
  98. package/src/services/thread/thread-title.service.ts +10 -4
  99. package/src/services/thread/thread-turn-execution.ts +43 -45
  100. package/src/services/thread/thread-turn-preparation.service.ts +257 -135
  101. package/src/services/thread/thread-turn-streaming.ts +82 -85
  102. package/src/services/thread/thread-turn.ts +8 -8
  103. package/src/services/thread/thread.service.ts +135 -100
  104. package/src/services/user.service.ts +45 -48
  105. package/src/storage/attachment-parser.ts +6 -2
  106. package/src/storage/attachment-storage.service.ts +5 -6
  107. package/src/storage/generated-document-storage.service.ts +1 -1
  108. package/src/system-agents/context-compaction.agent.ts +10 -9
  109. package/src/system-agents/delegated-agent-factory.ts +30 -6
  110. package/src/system-agents/memory-reranker.agent.ts +10 -9
  111. package/src/system-agents/memory.agent.ts +10 -9
  112. package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
  113. package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
  114. package/src/system-agents/skill-extractor.agent.ts +13 -12
  115. package/src/system-agents/skill-manager.agent.ts +13 -12
  116. package/src/system-agents/thread-router.agent.ts +10 -5
  117. package/src/system-agents/title-generator.agent.ts +13 -12
  118. package/src/tools/fetch-webpage.tool.ts +13 -13
  119. package/src/tools/memory-block.tool.ts +3 -1
  120. package/src/tools/plan-approval.tool.ts +4 -2
  121. package/src/tools/read-file-parts.tool.ts +10 -4
  122. package/src/tools/remember-memory.tool.ts +3 -1
  123. package/src/tools/research-topic.tool.ts +9 -5
  124. package/src/tools/search-web.tool.ts +16 -16
  125. package/src/tools/search.tool.ts +20 -5
  126. package/src/tools/team-think.tool.ts +61 -38
  127. package/src/utils/async.ts +5 -5
  128. package/src/utils/errors.ts +19 -18
  129. package/src/utils/sse-keepalive.ts +28 -25
  130. package/src/workers/bootstrap.ts +75 -11
  131. package/src/workers/memory-consolidation.worker.ts +82 -91
  132. package/src/workers/organization-learning.worker.ts +14 -4
  133. package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
  134. package/src/workers/skill-extraction.runner.ts +97 -61
  135. package/src/workers/utils/repo-structure-extractor.ts +13 -8
  136. package/src/workers/utils/thread-message-query.ts +24 -24
  137. package/src/workers/worker-utils.ts +23 -4
  138. package/src/effect/helpers.ts +0 -123
@@ -2,7 +2,7 @@ import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import { Duration, Effect, Schedule, Schema } from 'effect'
3
3
  import { z } from 'zod'
4
4
 
5
- import { iterateEffect } from '../../effect/helpers'
5
+ import { ERROR_TAGS } from '../../effect/errors'
6
6
  import { nowEpochMillis } from '../../utils/date-time'
7
7
  import {
8
8
  CHARS_PER_TOKEN_ESTIMATE,
@@ -60,12 +60,12 @@ export interface ContextCompactionRunnerParams {
60
60
  transcript: string
61
61
  }
62
62
 
63
- class CompactionError extends Schema.TaggedErrorClass<CompactionError>()('CompactionError', {
63
+ class CompactionError extends Schema.TaggedErrorClass<CompactionError>()(ERROR_TAGS.CompactionError, {
64
64
  message: Schema.String,
65
65
  cause: Schema.optional(Schema.Defect),
66
66
  }) {}
67
67
 
68
- class CompactionParseError extends Schema.TaggedErrorClass<CompactionParseError>()('CompactionParseError', {
68
+ class CompactionParseError extends Schema.TaggedErrorClass<CompactionParseError>()(ERROR_TAGS.CompactionParseError, {
69
69
  message: Schema.String,
70
70
  cause: Schema.optional(Schema.Defect),
71
71
  }) {}
@@ -88,9 +88,11 @@ export interface CompactionOutput {
88
88
  summary: string
89
89
  }
90
90
 
91
+ type ContextCompactionRunnerError = { readonly _tag: string; readonly message?: string }
92
+
91
93
  export type ContextCompactionRunner = (
92
94
  params: ContextCompactionRunnerParams,
93
- ) => Effect.Effect<CompactionOutput, unknown>
95
+ ) => Effect.Effect<CompactionOutput, ContextCompactionRunnerError, never>
94
96
 
95
97
  export interface CreateContextCompactionRuntimeOptions {
96
98
  runCompacter: ContextCompactionRunner
@@ -342,22 +344,29 @@ export function createContextCompactionRuntime(
342
344
  const chunks = splitByCharBudget(params.newMessages, compactionChunkMaxChars)
343
345
  const initialSummary = normalizeSummary(params.previousSummary)
344
346
 
345
- const finalSummary = yield* iterateEffect<{ summary: string; index: number }, CompactionError, never>(
346
- { summary: initialSummary, index: 0 },
347
- {
348
- while: (state) => state.index < chunks.length,
349
- body: (state) =>
350
- Effect.gen(function* () {
347
+ const stepSummary = (state: {
348
+ summary: string
349
+ index: number
350
+ }): Effect.Effect<{ summary: string; index: number }, CompactionError> =>
351
+ state.index >= chunks.length
352
+ ? Effect.succeed(state)
353
+ : Effect.gen(function* () {
351
354
  const chunk = chunks[state.index]
352
355
  const transcript = toCompactionTranscript(chunk)
353
356
  const output = yield* options.runCompacter({ previousSummary: state.summary, chunk, transcript }).pipe(
354
- Effect.mapError((error) => new CompactionError({ message: String(error), cause: error })),
357
+ Effect.mapError(
358
+ (error) =>
359
+ new CompactionError({
360
+ message: error.message ?? `Context compaction runner failed with ${error._tag}.`,
361
+ cause: error,
362
+ }),
363
+ ),
355
364
  Effect.retry(COMPACTION_RUNNER_RETRY_OPTIONS),
356
365
  )
357
- return { summary: normalizeSummary(output.summary), index: state.index + 1 }
358
- }),
359
- },
360
- )
366
+ return yield* stepSummary({ summary: normalizeSummary(output.summary), index: state.index + 1 })
367
+ })
368
+
369
+ const finalSummary = yield* stepSummary({ summary: initialSummary, index: 0 })
361
370
 
362
371
  return { summary: finalSummary.summary }
363
372
  })
@@ -419,59 +428,62 @@ export function createContextCompactionRuntime(
419
428
  done: false,
420
429
  }
421
430
 
422
- const finalState = yield* iterateEffect<CompactionLoopState, CompactionError, never>(initialState, {
423
- while: (state) => !state.done,
424
- body: (state) =>
425
- Effect.gen(function* () {
426
- const assessment = shouldCompactHistory({
427
- summaryText: state.summaryText,
428
- liveMessages: state.remainingMessages,
429
- contextSize: params.contextSize,
431
+ const stepHistory = (state: CompactionLoopState): Effect.Effect<CompactionLoopState, CompactionError> =>
432
+ state.done
433
+ ? Effect.succeed(state)
434
+ : Effect.gen(function* () {
435
+ const assessment = shouldCompactHistory({
436
+ summaryText: state.summaryText,
437
+ liveMessages: state.remainingMessages,
438
+ contextSize: params.contextSize,
439
+ })
440
+
441
+ if (!assessment.shouldCompact) {
442
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
443
+ }
444
+
445
+ const boundary = Math.max(0, state.remainingMessages.length - params.tailMessageCount)
446
+ if (boundary <= 0) {
447
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
448
+ }
449
+
450
+ const candidatePrefix = state.remainingMessages.slice(0, boundary)
451
+ const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
452
+ const contextMessages = messagesToCompact
453
+ .map(toContextMessageFromChatMessage)
454
+ .filter((message) => compactWhitespace(message.text).length > 0)
455
+ const sourceText = toCompactionTranscript(contextMessages)
456
+
457
+ if (!compactWhitespace(sourceText)) {
458
+ return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
459
+ }
460
+
461
+ const compacted = yield* compactContextMessagesEffect({
462
+ previousSummary: state.summaryText,
463
+ newMessages: contextMessages,
464
+ })
465
+ const rolledSummary = yield* rollupSummaryIfOversizedEffect(normalizeSummary(compacted.summary))
466
+
467
+ if (rolledSummary.length >= sourceText.length) {
468
+ return yield* new CompactionError({
469
+ message: 'Compaction summary is not shorter than compacted source',
470
+ })
471
+ }
472
+
473
+ return yield* stepHistory({
474
+ summaryText: rolledSummary,
475
+ remainingMessages: state.remainingMessages.slice(boundary),
476
+ compactedMessages: [
477
+ ...state.compactedMessages,
478
+ ...candidatePrefix.map((message) => markMessageCompacted(message, now)),
479
+ ],
480
+ lastCompactedMessageId: candidatePrefix.at(-1)?.id ?? state.lastCompactedMessageId,
481
+ estimatedTokens: assessment.estimatedTokens,
482
+ done: false,
483
+ })
430
484
  })
431
485
 
432
- if (!assessment.shouldCompact) {
433
- return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
434
- }
435
-
436
- const boundary = Math.max(0, state.remainingMessages.length - params.tailMessageCount)
437
- if (boundary <= 0) {
438
- return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
439
- }
440
-
441
- const candidatePrefix = state.remainingMessages.slice(0, boundary)
442
- const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
443
- const contextMessages = messagesToCompact
444
- .map(toContextMessageFromChatMessage)
445
- .filter((message) => compactWhitespace(message.text).length > 0)
446
- const sourceText = toCompactionTranscript(contextMessages)
447
-
448
- if (!compactWhitespace(sourceText)) {
449
- return { ...state, estimatedTokens: assessment.estimatedTokens, done: true }
450
- }
451
-
452
- const compacted = yield* compactContextMessagesEffect({
453
- previousSummary: state.summaryText,
454
- newMessages: contextMessages,
455
- })
456
- const rolledSummary = yield* rollupSummaryIfOversizedEffect(normalizeSummary(compacted.summary))
457
-
458
- if (rolledSummary.length >= sourceText.length) {
459
- return yield* new CompactionError({ message: 'Compaction summary is not shorter than compacted source' })
460
- }
461
-
462
- return {
463
- summaryText: rolledSummary,
464
- remainingMessages: state.remainingMessages.slice(boundary),
465
- compactedMessages: [
466
- ...state.compactedMessages,
467
- ...candidatePrefix.map((message) => markMessageCompacted(message, now)),
468
- ],
469
- lastCompactedMessageId: candidatePrefix.at(-1)?.id ?? state.lastCompactedMessageId,
470
- estimatedTokens: assessment.estimatedTokens,
471
- done: false,
472
- }
473
- }),
474
- })
486
+ const finalState = yield* stepHistory(initialState)
475
487
 
476
488
  return buildExitResult(finalState)
477
489
  })
@@ -10,7 +10,7 @@
10
10
  import type { Layer as LayerType } from 'effect'
11
11
  import { Layer } from 'effect'
12
12
 
13
- import { AiGatewayLive } from '../ai-gateway/ai-gateway'
13
+ import type { AiGatewayModelsTag, AiGatewayTag, RuntimeBridgeTag } from '../ai-gateway/ai-gateway'
14
14
  import { EmbeddingCacheLive } from '../ai/embedding-cache'
15
15
  import type { buildInfrastructureLayer } from '../effect/layers'
16
16
  import { LotaQueuesLive } from '../queues/queues.service'
@@ -85,12 +85,18 @@ function provide<A, E, R extends RCtx, RCtx, E2>(
85
85
 
86
86
  type InfrastructureLayer = ReturnType<typeof buildInfrastructureLayer>
87
87
 
88
+ type BridgeLayer = LayerType.Layer<AiGatewayTag | AiGatewayModelsTag | RuntimeBridgeTag, never, never>
89
+
88
90
  /**
89
91
  * Compose the domain service layer tree on top of the supplied infrastructure
90
- * layer. The returned layer has no remaining requirements and can be fed
91
- * straight into `ManagedRuntime.make`.
92
+ * layer. `bridgeLayer` provides `AiGatewayModelsTag` and `RuntimeBridgeTag`
93
+ * these are constructed at the host boundary (`createLotaRuntime`) via
94
+ * `Deferred` because they reference the `ManagedRuntime` that is being built.
95
+ * They are threaded into the domain context so service layers that need them
96
+ * can `yield*` the tags directly instead of prop-drilling `runPromise`.
92
97
  */
93
- export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer) {
98
+ export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer, bridgeLayer: BridgeLayer) {
99
+ const baseCtx = Layer.mergeAll(infrastructureLayer, bridgeLayer)
94
100
  const tier0 = provide(
95
101
  Layer.mergeAll(
96
102
  BackgroundWorkServiceLive,
@@ -103,11 +109,11 @@ export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer
103
109
  PlanBuilderServiceLive,
104
110
  WriteIntentValidatorServiceLive,
105
111
  ),
106
- infrastructureLayer,
112
+ baseCtx,
107
113
  )
108
114
  const ctx0 = Layer.mergeAll(
109
- infrastructureLayer,
110
- provide(Layer.mergeAll(AiGatewayLive, EmbeddingCacheLive, FirecrawlLive, HelperModelLive), infrastructureLayer),
115
+ baseCtx,
116
+ provide(Layer.mergeAll(EmbeddingCacheLive, FirecrawlLive, HelperModelLive), baseCtx),
111
117
  tier0,
112
118
  )
113
119
 
@@ -26,7 +26,7 @@ function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): stri
26
26
  }
27
27
 
28
28
  export class ExecutionPlanCacheError extends Schema.TaggedErrorClass<ExecutionPlanCacheError>()(
29
- 'ExecutionPlanCacheError',
29
+ '@lota-sdk/core/ExecutionPlanCacheError',
30
30
  { message: Schema.String, cause: Schema.Defect },
31
31
  ) {}
32
32
 
@@ -42,7 +42,11 @@ export function buildExecutionPlanInstructionSections(plans: SerializableExecuti
42
42
  export function createExecutionPlanInstructionSectionCache(params: {
43
43
  disabled?: boolean
44
44
  loadPlansEffect: () => Effect.Effect<SerializableExecutionPlan[], ExecutionPlanCacheError>
45
+ /** Promise adapter used by the `getPlans()` / `getSections()` Promise API.
46
+ * Required so this module never reaches for an ambient `Effect.runPromise`. */
47
+ runPromise: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>
45
48
  }) {
49
+ const runPromise = params.runPromise
46
50
  const [getPlansCachedEffect, invalidatePlansEffect] = Effect.runSync(
47
51
  Effect.cachedInvalidateWithTTL(
48
52
  Effect.suspend(() => (params.disabled ? Effect.succeed([]) : params.loadPlansEffect())),
@@ -75,10 +79,10 @@ export function createExecutionPlanInstructionSectionCache(params: {
75
79
  getPlansEffect,
76
80
  getSectionsEffect,
77
81
  getPlans() {
78
- return Effect.runPromise(getPlansEffect())
82
+ return runPromise(getPlansEffect())
79
83
  },
80
84
  getSections() {
81
- return Effect.runPromise(getSectionsEffect())
85
+ return runPromise(getSectionsEffect())
82
86
  },
83
87
  }
84
88
  }
@@ -5,7 +5,7 @@ import { nowDate } from '../../utils/date-time'
5
5
  import { compactWhitespace } from '../../utils/string'
6
6
 
7
7
  export class MemoryBlockCompactError extends Schema.TaggedErrorClass<MemoryBlockCompactError>()(
8
- 'MemoryBlockCompactError',
8
+ '@lota-sdk/core/MemoryBlockCompactError',
9
9
  { message: Schema.String, cause: Schema.Defect },
10
10
  ) {}
11
11
 
@@ -65,7 +65,7 @@ export interface CompactMemoryBlockEntriesParams {
65
65
  compact: (params: {
66
66
  previousSummary: string
67
67
  newEntriesText: string
68
- }) => PromiseLike<string> | Effect.Effect<string, MemoryBlockCompactError>
68
+ }) => Effect.Effect<string, MemoryBlockCompactError>
69
69
  }
70
70
 
71
71
  export interface CompactMemoryBlockEntriesResult {
@@ -119,13 +119,7 @@ export function compactMemoryBlockEntries(
119
119
  while (entries.length >= params.triggerEntries) {
120
120
  const chunk = entries.slice(0, params.chunkEntries)
121
121
  const compactParams = { previousSummary: summary, newEntriesText: toMemoryBlockEntriesSource(chunk) }
122
- const value = params.compact(compactParams)
123
- const raw = yield* Effect.isEffect(value)
124
- ? value
125
- : Effect.tryPromise({
126
- try: () => Promise.resolve(value),
127
- catch: (cause) => new MemoryBlockCompactError({ message: 'compact callback failed', cause }),
128
- })
122
+ const raw = yield* params.compact(compactParams)
129
123
  const nextSummary = raw.trim()
130
124
 
131
125
  if (!nextSummary) {
@@ -1,11 +1,13 @@
1
1
  import { Effect, Schema } from 'effect'
2
2
 
3
+ import { ERROR_TAGS } from '../../effect/errors'
4
+
3
5
  export const ORG_SCOPE_PREFIX = 'org'
4
6
 
5
7
  const SCOPE_ID_MAX_LENGTH = 255
6
8
  const SCOPE_PREFIX_REGEX = /^[a-z][a-z0-9_]*$/
7
9
 
8
- export class MemoryScopeError extends Schema.TaggedErrorClass<MemoryScopeError>()('MemoryScopeError', {
10
+ export class MemoryScopeError extends Schema.TaggedErrorClass<MemoryScopeError>()(ERROR_TAGS.MemoryScopeError, {
9
11
  message: Schema.String,
10
12
  }) {}
11
13
 
@@ -1,13 +1,14 @@
1
1
  import { Schema, Effect } from 'effect'
2
2
 
3
3
  import type { RecordIdRef } from '../db/record-id'
4
+ import { ERROR_TAGS } from '../effect/errors'
4
5
  import type { LotaRuntimeAdapters, LotaRuntimeIndexedRepositoriesContext } from './runtime-extensions'
5
6
 
6
7
  function isRecord(value: unknown): value is Record<string, unknown> {
7
8
  return typeof value === 'object' && value !== null
8
9
  }
9
10
 
10
- class PluginResolutionError extends Schema.TaggedErrorClass<PluginResolutionError>()('PluginResolutionError', {
11
+ class PluginResolutionError extends Schema.TaggedErrorClass<PluginResolutionError>()(ERROR_TAGS.PluginResolutionError, {
11
12
  stage: Schema.Literals([
12
13
  'configuration',
13
14
  'linear-installation',
@@ -3,6 +3,7 @@ import { Schema, Effect } from 'effect'
3
3
 
4
4
  import type { ResolvedAgentConfig } from '../config/agent-defaults'
5
5
  import type { RecordIdRef } from '../db/record-id'
6
+ import { ERROR_TAGS } from '../effect/errors'
6
7
  import { AgentConfigServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
7
8
  import { LotaQueuesServiceTag } from '../queues/queues.service'
8
9
  import { RecentActivityServiceTag } from '../services/recent-activity.service'
@@ -27,10 +28,10 @@ import {
27
28
  toHistoryMessages,
28
29
  } from './thread-chat-helpers'
29
30
 
30
- class PostTurnSideEffectsError extends Schema.TaggedErrorClass<PostTurnSideEffectsError>()('PostTurnSideEffectsError', {
31
- message: Schema.String,
32
- cause: Schema.Defect,
33
- }) {}
31
+ class PostTurnSideEffectsError extends Schema.TaggedErrorClass<PostTurnSideEffectsError>()(
32
+ ERROR_TAGS.PostTurnSideEffectsError,
33
+ { message: Schema.String, cause: Schema.Defect },
34
+ ) {}
34
35
 
35
36
  function tryPostTurnSideEffect<A>(
36
37
  message: string,
@@ -214,7 +215,7 @@ export const runPostTurnSideEffects = Effect.fn('PostTurnSideEffects.run')(funct
214
215
  () => {
215
216
  const enqueuePostChatOrgAction = runtimeAdapters.enqueuePostChatOrgAction
216
217
  if (!enqueuePostChatOrgAction) {
217
- return
218
+ return Promise.resolve()
218
219
  }
219
220
 
220
221
  const sourceCreatedAt = referenceUserMessage.metadata?.createdAt ?? nowEpochMillis()
@@ -1,8 +1,10 @@
1
1
  import { Schema, Effect } from 'effect'
2
2
 
3
+ import { ERROR_TAGS } from '../effect/errors'
4
+
3
5
  interface ScopedRetrievalTask<TCandidate, E = never> {
4
6
  scopeTag: string
5
- retrieve: () => PromiseLike<TCandidate[]> | Effect.Effect<TCandidate[], E>
7
+ retrieve: () => Effect.Effect<TCandidate[], E>
6
8
  }
7
9
 
8
10
  interface ScopedRetrievalResult<TCandidate> {
@@ -10,7 +12,7 @@ interface ScopedRetrievalResult<TCandidate> {
10
12
  candidates: TCandidate[]
11
13
  }
12
14
 
13
- class ScopedRetrievalError extends Schema.TaggedErrorClass<ScopedRetrievalError>()('ScopedRetrievalError', {
15
+ class ScopedRetrievalError extends Schema.TaggedErrorClass<ScopedRetrievalError>()(ERROR_TAGS.ScopedRetrievalError, {
14
16
  scopeTag: Schema.String,
15
17
  message: Schema.String,
16
18
  cause: Schema.Defect,
@@ -26,24 +28,10 @@ export function executeScopedRetrieval<TCandidate, E>(
26
28
  return Effect.forEach(
27
29
  tasks,
28
30
  (task) =>
29
- Effect.suspend(() => {
30
- try {
31
- const result = task.retrieve()
32
- if (Effect.isEffect(result)) {
33
- return result.pipe(
34
- Effect.mapError((cause) => toScopedRetrievalError(task.scopeTag, cause)),
35
- Effect.map((candidates) => ({ scopeTag: task.scopeTag, candidates })),
36
- )
37
- }
38
-
39
- return Effect.tryPromise({
40
- try: () => result,
41
- catch: (cause) => toScopedRetrievalError(task.scopeTag, cause),
42
- }).pipe(Effect.map((candidates) => ({ scopeTag: task.scopeTag, candidates })))
43
- } catch (cause) {
44
- return Effect.fail(toScopedRetrievalError(task.scopeTag, cause))
45
- }
46
- }),
31
+ task.retrieve().pipe(
32
+ Effect.mapError((cause) => toScopedRetrievalError(task.scopeTag, cause)),
33
+ Effect.map((candidates) => ({ scopeTag: task.scopeTag, candidates })),
34
+ ),
47
35
  { concurrency: 'unbounded' },
48
36
  )
49
37
  }
@@ -105,16 +105,10 @@ export interface LotaRuntimeSocialChatConfig {
105
105
  slack?: LotaSocialChatSlackConfig
106
106
  historyRedisKeyPrefix?: string
107
107
  stateRedisKeyPrefix?: string
108
- resolveContext: (
109
- params: LotaSocialChatResolveContextParams,
110
- ) => LotaSocialChatResolvedContext | Promise<LotaSocialChatResolvedContext>
111
- buildAgentTools: (params: BuildSocialChatAgentToolsParams) => ToolSet | Promise<ToolSet>
108
+ resolveContext: (params: LotaSocialChatResolveContextParams) => Promise<LotaSocialChatResolvedContext>
109
+ buildAgentTools: (params: BuildSocialChatAgentToolsParams) => Promise<ToolSet>
112
110
  getConsultParticipants?:
113
- | ((params: {
114
- workspaceId: RecordIdRef
115
- workspaceIdString: string
116
- platform: 'slack'
117
- }) => string[] | Promise<string[]>)
111
+ | ((params: { workspaceId: RecordIdRef; workspaceIdString: string; platform: 'slack' }) => Promise<string[]>)
118
112
  | undefined
119
113
  }
120
114
 
@@ -35,12 +35,10 @@ export interface LotaRuntimeProfileProjection {
35
35
 
36
36
  export interface LotaRuntimeWorkspaceProvider {
37
37
  getWorkspace(workspaceId: RecordIdRef): Promise<Record<string, unknown>>
38
- getLifecycleState?(
39
- workspace: Record<string, unknown>,
40
- ): Promise<LotaRuntimeWorkspaceLifecycleState> | LotaRuntimeWorkspaceLifecycleState
38
+ getLifecycleState?(workspace: Record<string, unknown>): Promise<LotaRuntimeWorkspaceLifecycleState>
41
39
  readProfileProjectionState?(
42
40
  workspace: Record<string, unknown>,
43
- ): Promise<LotaRuntimeWorkspaceProjectionState | undefined> | LotaRuntimeWorkspaceProjectionState | undefined
41
+ ): Promise<LotaRuntimeWorkspaceProjectionState | undefined>
44
42
  buildPromptSummary?(workspaceId: RecordIdRef): Promise<string | undefined>
45
43
  listRecentDomainEvents?(workspaceId: RecordIdRef, limit?: number): Promise<Array<Record<string, unknown>>>
46
44
  hasActiveKnowledgeSources?(workspaceId: string): Promise<boolean>
@@ -3,18 +3,15 @@
3
3
  *
4
4
  * The plugin connect/disconnect helpers run inside the supplied
5
5
  * `ManagedRuntime` so spans, logging, and layer context stay consistent with
6
- * the rest of the SDK runtime. `createRuntimeDisconnect` instead drives the
7
- * shutdown sequence through the caller-supplied `runPromiseWithCurrentContext`
8
- * (currently `Effect.runPromiseWith(Context.empty())`), which intentionally
9
- * does not preserve the SDK runtime context — by then the runtime is being
10
- * torn down.
6
+ * the rest of the SDK runtime. Disconnect runs its teardown sequence as a
7
+ * plain `Effect.runPromise` the services it yields have no layer
8
+ * requirements because they are closure-captured shutdown thunks.
11
9
  */
12
10
 
13
11
  import type { ManagedRuntime } from 'effect'
14
12
  import { Effect } from 'effect'
15
13
 
16
- import { clearAiGatewayRuntime } from '../ai-gateway/ai-gateway'
17
- import { effectTryPromise } from '../effect/helpers'
14
+ import { RuntimeLifecycleError } from '../effect/errors'
18
15
  import type { LotaPlugin } from './plugin-types'
19
16
 
20
17
  // eslint-disable-next-line typescript-eslint/no-explicit-any -- ManagedRuntime is contravariant in R; `any` is the only valid wildcard
@@ -24,6 +21,26 @@ function getPluginLifecycleServices(plugin: LotaPlugin): Record<string, unknown>
24
21
  return plugin.services
25
22
  }
26
23
 
24
+ function toRuntimeLifecycleError(
25
+ operation: RuntimeLifecycleError['operation'],
26
+ cause: unknown,
27
+ pluginName?: string,
28
+ ): RuntimeLifecycleError {
29
+ const detail = pluginName ? ` "${pluginName}"` : ''
30
+ const action =
31
+ operation === 'connect-plugin-database'
32
+ ? `connect plugin database${detail}`
33
+ : operation === 'disconnect-plugin-database'
34
+ ? `disconnect plugin database${detail}`
35
+ : operation === 'shutdown-social-chat'
36
+ ? 'shut down social chat'
37
+ : operation === 'disconnect-plugin-databases'
38
+ ? 'disconnect plugin databases'
39
+ : 'dispose managed runtime'
40
+
41
+ return new RuntimeLifecycleError({ operation, message: `Failed to ${action}.`, cause })
42
+ }
43
+
27
44
  /**
28
45
  * Build a plugin database connector that iterates the configured plugins and
29
46
  * calls each `services.connectDatabase()` once, tracking completion in
@@ -49,7 +66,10 @@ export function createPluginDatabaseConnector(
49
66
  }
50
67
 
51
68
  const connectDatabaseFn = connectDatabase as (this: typeof services) => Promise<void>
52
- yield* effectTryPromise(() => connectDatabaseFn.call(services))
69
+ yield* Effect.tryPromise({
70
+ try: () => connectDatabaseFn.call(services),
71
+ catch: (cause) => toRuntimeLifecycleError('connect-plugin-database', cause, pluginName),
72
+ })
53
73
  connectedPluginDatabases.add(pluginName)
54
74
  }
55
75
  }),
@@ -82,7 +102,10 @@ export function createPluginDatabaseDisconnector(
82
102
  }
83
103
 
84
104
  const disconnectDatabaseFn = disconnectDatabase as (this: typeof services) => Promise<void>
85
- yield* effectTryPromise(() => disconnectDatabaseFn.call(services))
105
+ yield* Effect.tryPromise({
106
+ try: () => disconnectDatabaseFn.call(services),
107
+ catch: (cause) => toRuntimeLifecycleError('disconnect-plugin-database', cause, pluginName),
108
+ })
86
109
  connectedPluginDatabases.delete(pluginName)
87
110
  }
88
111
  }),
@@ -91,7 +114,6 @@ export function createPluginDatabaseDisconnector(
91
114
 
92
115
  interface CreateDisconnectInput {
93
116
  managedRuntime: SdkManagedRuntime
94
- runPromiseWithCurrentContext: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>
95
117
  socialChatShutdown: () => Promise<void>
96
118
  disconnectPluginDatabases: () => Promise<void>
97
119
  }
@@ -100,9 +122,15 @@ interface CreateDisconnectInput {
100
122
  * Compose the runtime `disconnect()` function. The returned function is
101
123
  * idempotent: the first call starts the shutdown sequence, subsequent calls
102
124
  * return the same in-flight promise.
125
+ *
126
+ * Teardown order:
127
+ * 1. socialChat shutdown (user-facing bots must go quiet first)
128
+ * 2. plugin database disconnect (release external connections)
129
+ * 3. clear AI gateway runtime slot BEFORE dispose (no new fibers)
130
+ * 4. managedRuntime.dispose() (release all scoped layer resources)
103
131
  */
104
132
  export function createRuntimeDisconnect(input: CreateDisconnectInput): () => Promise<void> {
105
- const { managedRuntime, runPromiseWithCurrentContext, socialChatShutdown, disconnectPluginDatabases } = input
133
+ const { managedRuntime, socialChatShutdown, disconnectPluginDatabases } = input
106
134
 
107
135
  let disconnectPromise: Promise<void> | null = null
108
136
 
@@ -111,12 +139,24 @@ export function createRuntimeDisconnect(input: CreateDisconnectInput): () => Pro
111
139
  return disconnectPromise
112
140
  }
113
141
 
114
- disconnectPromise = runPromiseWithCurrentContext(
142
+ disconnectPromise = Effect.runPromise(
115
143
  Effect.gen(function* () {
116
- yield* Effect.ignore(effectTryPromise(() => socialChatShutdown()))
117
- yield* Effect.ignore(effectTryPromise(() => disconnectPluginDatabases()))
118
- yield* effectTryPromise(() => managedRuntime.dispose())
119
- yield* Effect.sync(() => clearAiGatewayRuntime())
144
+ yield* Effect.ignore(
145
+ Effect.tryPromise({
146
+ try: () => socialChatShutdown(),
147
+ catch: (cause) => toRuntimeLifecycleError('shutdown-social-chat', cause),
148
+ }),
149
+ )
150
+ yield* Effect.ignore(
151
+ Effect.tryPromise({
152
+ try: () => disconnectPluginDatabases(),
153
+ catch: (cause) => toRuntimeLifecycleError('disconnect-plugin-databases', cause),
154
+ }),
155
+ )
156
+ yield* Effect.tryPromise({
157
+ try: () => managedRuntime.dispose(),
158
+ catch: (cause) => toRuntimeLifecycleError('dispose-managed-runtime', cause),
159
+ })
120
160
  }),
121
161
  )
122
162
  return disconnectPromise