@lota-sdk/core 0.4.10 → 0.4.12

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 (110) hide show
  1. package/package.json +3 -3
  2. package/src/ai-gateway/ai-gateway.ts +214 -98
  3. package/src/ai-gateway/index.ts +16 -1
  4. package/src/config/agent-defaults.ts +4 -120
  5. package/src/config/logger.ts +18 -34
  6. package/src/config/model-constants.ts +1 -0
  7. package/src/config/thread-defaults.ts +1 -18
  8. package/src/create-runtime.ts +90 -28
  9. package/src/db/base.service.ts +30 -38
  10. package/src/db/service.ts +489 -545
  11. package/src/effect/index.ts +0 -2
  12. package/src/effect/layers.ts +6 -13
  13. package/src/embeddings/provider.ts +2 -7
  14. package/src/index.ts +4 -5
  15. package/src/queues/autonomous-job.queue.ts +159 -113
  16. package/src/queues/context-compaction.queue.ts +39 -25
  17. package/src/queues/delayed-node-promotion.queue.ts +56 -29
  18. package/src/queues/document-processor.queue.ts +5 -3
  19. package/src/queues/index.ts +1 -0
  20. package/src/queues/memory-consolidation.queue.ts +79 -53
  21. package/src/queues/organization-learning.queue.ts +63 -39
  22. package/src/queues/plan-agent-heartbeat.queue.ts +104 -79
  23. package/src/queues/plan-scheduler.queue.ts +100 -84
  24. package/src/queues/post-chat-memory.queue.ts +55 -33
  25. package/src/queues/queue-factory.ts +40 -41
  26. package/src/queues/queues.service.ts +61 -0
  27. package/src/queues/title-generation.queue.ts +42 -31
  28. package/src/redis/org-memory-lock.ts +24 -9
  29. package/src/redis/redis-lease-lock.ts +8 -1
  30. package/src/runtime/agent-identity-overrides.ts +7 -3
  31. package/src/runtime/agent-runtime-policy.ts +9 -4
  32. package/src/runtime/agent-stream-helpers.ts +9 -4
  33. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  34. package/src/runtime/context-compaction/context-compaction.ts +9 -7
  35. package/src/runtime/domain-layer.ts +15 -4
  36. package/src/runtime/execution-plan-visibility.ts +5 -2
  37. package/src/runtime/graph-designer.ts +0 -22
  38. package/src/runtime/index.ts +2 -0
  39. package/src/runtime/indexed-repositories-policy.ts +2 -6
  40. package/src/runtime/live-turn-trace.ts +344 -0
  41. package/src/runtime/plugin-resolution.ts +29 -12
  42. package/src/runtime/post-turn-side-effects.ts +139 -141
  43. package/src/runtime/runtime-config.ts +0 -6
  44. package/src/runtime/runtime-extensions.ts +0 -54
  45. package/src/runtime/runtime-lifecycle.ts +4 -4
  46. package/src/runtime/runtime-services.ts +125 -53
  47. package/src/runtime/runtime-worker-registry.ts +113 -30
  48. package/src/runtime/social-chat/social-chat-agent-runner.ts +6 -3
  49. package/src/runtime/social-chat/social-chat-history.ts +3 -1
  50. package/src/runtime/social-chat/social-chat.ts +35 -20
  51. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +6 -5
  52. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  53. package/src/runtime/thread-chat-helpers.ts +18 -9
  54. package/src/runtime/thread-turn-context.ts +7 -47
  55. package/src/runtime/turn-lifecycle.ts +6 -14
  56. package/src/services/agent-activity.service.ts +168 -175
  57. package/src/services/agent-executor.service.ts +35 -16
  58. package/src/services/attachment.service.ts +4 -70
  59. package/src/services/autonomous-job.service.ts +53 -61
  60. package/src/services/context-compaction.service.ts +7 -9
  61. package/src/services/execution-plan/execution-plan-graph.ts +106 -115
  62. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  63. package/src/services/execution-plan/execution-plan.service.ts +67 -50
  64. package/src/services/global-orchestrator.service.ts +18 -7
  65. package/src/services/graph-full-routing.ts +7 -6
  66. package/src/services/memory/memory-conversation.ts +10 -5
  67. package/src/services/memory/memory.service.ts +11 -8
  68. package/src/services/ownership-dispatcher.service.ts +16 -5
  69. package/src/services/plan/plan-agent-heartbeat.service.ts +29 -15
  70. package/src/services/plan/plan-agent-query.service.ts +12 -8
  71. package/src/services/plan/plan-completion-side-effects.ts +93 -101
  72. package/src/services/plan/plan-cycle.service.ts +7 -45
  73. package/src/services/plan/plan-deadline.service.ts +28 -17
  74. package/src/services/plan/plan-event-delivery.service.ts +47 -40
  75. package/src/services/plan/plan-executor-context.ts +2 -0
  76. package/src/services/plan/plan-executor-graph.ts +366 -391
  77. package/src/services/plan/plan-executor.service.ts +13 -91
  78. package/src/services/plan/plan-scheduler.service.ts +62 -49
  79. package/src/services/plan/plan-transaction-events.ts +1 -1
  80. package/src/services/recent-activity-title.service.ts +6 -2
  81. package/src/services/thread/thread-bootstrap.ts +11 -9
  82. package/src/services/thread/thread-message.service.ts +6 -5
  83. package/src/services/thread/thread-turn-execution.ts +86 -82
  84. package/src/services/thread/thread-turn-preparation.service.ts +92 -45
  85. package/src/services/thread/thread-turn-streaming.ts +60 -28
  86. package/src/services/thread/thread-turn.ts +212 -46
  87. package/src/services/thread/thread.service.ts +21 -6
  88. package/src/system-agents/recent-activity-title-refiner.agent.ts +8 -5
  89. package/src/system-agents/thread-router.agent.ts +23 -20
  90. package/src/tools/execution-plan.tool.ts +8 -3
  91. package/src/tools/fetch-webpage.tool.ts +10 -9
  92. package/src/tools/firecrawl-client.ts +0 -15
  93. package/src/tools/remember-memory.tool.ts +3 -6
  94. package/src/tools/research-topic.tool.ts +12 -3
  95. package/src/tools/search-web.tool.ts +10 -9
  96. package/src/tools/search.tool.ts +4 -5
  97. package/src/tools/team-think.tool.ts +139 -121
  98. package/src/workers/bootstrap.ts +9 -10
  99. package/src/workers/memory-consolidation.worker.ts +4 -1
  100. package/src/workers/organization-learning.worker.ts +15 -2
  101. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  102. package/src/workers/regular-chat-memory-digest.runner.ts +21 -14
  103. package/src/workers/skill-extraction.runner.ts +13 -15
  104. package/src/workers/worker-utils.ts +6 -18
  105. package/src/effect/awaitable-effect.ts +0 -96
  106. package/src/effect/runtime-ref.ts +0 -25
  107. package/src/effect/runtime.ts +0 -46
  108. package/src/redis/runtime-connection.ts +0 -20
  109. package/src/runtime/runtime-accessors.ts +0 -92
  110. package/src/runtime/runtime-token.ts +0 -47
@@ -5,22 +5,9 @@ import { Effect } from 'effect'
5
5
  import type { Context } from 'effect'
6
6
 
7
7
  import { chatLogger } from '../config/logger'
8
- import { resolveLotaService } from '../effect/runtime'
9
- import { QueueJobServiceTag } from '../services/queue-job.service'
8
+ import type { QueueJobServiceTag } from '../services/queue-job.service'
10
9
 
11
- let currentQueueJobService: Context.Service.Shape<typeof QueueJobServiceTag> | null = null
12
-
13
- export function configureQueueJobService(service: Context.Service.Shape<typeof QueueJobServiceTag>): void {
14
- currentQueueJobService = service
15
- }
16
-
17
- export function clearQueueJobService(): void {
18
- currentQueueJobService = null
19
- }
20
-
21
- export function getQueueJobService(): Context.Service.Shape<typeof QueueJobServiceTag> {
22
- return currentQueueJobService ?? resolveLotaService(QueueJobServiceTag)
23
- }
10
+ export type QueueJobService = Context.Service.Shape<typeof QueueJobServiceTag>
24
11
 
25
12
  export const DEFAULT_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 24 * 60 * 60, count: 200 } }
26
13
  export const LOW_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 6 * 60 * 60, count: 50 } }
@@ -98,6 +85,7 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
98
85
  export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TResult = void>(
99
86
  queueName: string,
100
87
  processor: (job: TJob) => Promise<TResult>,
88
+ queueJobService: QueueJobService,
101
89
  ): (job: TJob) => Promise<TResult> {
102
90
  return (job: TJob) => {
103
91
  const trackedJob = {
@@ -117,7 +105,7 @@ export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TR
117
105
  return Effect.runPromise(
118
106
  Effect.catch(
119
107
  Effect.gen(function* () {
120
- yield* Effect.catch(getQueueJobService().markAttemptStarted(trackedJob), (error) =>
108
+ yield* Effect.catch(queueJobService.markAttemptStarted(trackedJob), (error) =>
121
109
  Effect.sync(() => {
122
110
  logPersistenceError('start', error)
123
111
  }),
@@ -125,7 +113,7 @@ export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TR
125
113
 
126
114
  const result = yield* Effect.tryPromise(() => processor(job))
127
115
 
128
- yield* Effect.catch(getQueueJobService().markAttemptCompleted(trackedJob, result), (error) =>
116
+ yield* Effect.catch(queueJobService.markAttemptCompleted(trackedJob, result), (error) =>
129
117
  Effect.sync(() => {
130
118
  logPersistenceError('completion', error)
131
119
  }),
@@ -135,7 +123,7 @@ export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TR
135
123
  }),
136
124
  (error) =>
137
125
  Effect.gen(function* () {
138
- yield* Effect.catch(getQueueJobService().markAttemptFailed(trackedJob, error), (persistenceError) =>
126
+ yield* Effect.catch(queueJobService.markAttemptFailed(trackedJob, error), (persistenceError) =>
139
127
  Effect.sync(() => {
140
128
  logPersistenceError('failure', persistenceError)
141
129
  }),
@@ -1,96 +0,0 @@
1
- import { Effect } from 'effect'
2
-
3
- export type AwaitableEffect<A, E> = Effect.Effect<A, E, never> & Promise<A>
4
-
5
- export type AwaitableValue<T> = T extends (...args: infer TArgs) => Effect.Effect<infer A, infer E, never>
6
- ? (...args: TArgs) => AwaitableEffect<A, E>
7
- : T extends (...args: infer TArgs) => infer TResult
8
- ? (...args: TArgs) => TResult
9
- : T
10
-
11
- type AwaitableServiceMethods<T> = {
12
- [K in keyof T]: AwaitableValue<T[K]>
13
- }
14
-
15
- export type AwaitableService<T> = AwaitableServiceMethods<T>
16
-
17
- export type MaybeAwaitableService<T extends object> = T | AwaitableService<T>
18
-
19
- type AwaitableEffectOptions = { runPromise?: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A> }
20
-
21
- type Callable = (...args: Array<unknown>) => unknown
22
-
23
- function isCallable(value: unknown): value is Callable {
24
- return typeof value === 'function'
25
- }
26
-
27
- function isRuntimeEffect(value: unknown): value is Effect.Effect<unknown, never, never> {
28
- return Effect.isEffect(value)
29
- }
30
-
31
- /**
32
- * Wraps an Effect so it can be `await`-ed as if it were a Promise, while still
33
- * passing `Effect.isEffect` checks and exposing the underlying Effect properties.
34
- *
35
- * Implementation: returns a `Proxy` around the Effect that only intercepts the
36
- * PromiseLike methods (`then`/`catch`/`finally`) plus `Symbol.toStringTag`. The
37
- * Promise is created lazily on first `await` and cached for the wrapper's
38
- * lifetime so repeated awaits share a result. The original Effect object is
39
- * never mutated.
40
- */
41
- export function toAwaitableEffect<A, E>(
42
- effect: Effect.Effect<A, E, never>,
43
- options?: AwaitableEffectOptions,
44
- ): AwaitableEffect<A, E> {
45
- const runPromise = options?.runPromise ?? Effect.runPromise
46
- let cachedPromise: Promise<A> | undefined
47
- const getPromise = (): Promise<A> => (cachedPromise ??= runPromise(effect))
48
-
49
- const then: Promise<A>['then'] = (onFulfilled, onRejected) => getPromise().then(onFulfilled, onRejected)
50
- const catchMethod: Promise<A>['catch'] = (onRejected) => getPromise().catch(onRejected)
51
- const finallyMethod: Promise<A>['finally'] = (onFinally) => getPromise().finally(onFinally)
52
-
53
- return new Proxy(effect as object, {
54
- get(target, property, receiver) {
55
- if (property === 'then') return then
56
- if (property === 'catch') return catchMethod
57
- if (property === 'finally') return finallyMethod
58
- if (property === Symbol.toStringTag) return 'Promise'
59
- return Reflect.get(target, property, receiver) as unknown
60
- },
61
- }) as AwaitableEffect<A, E>
62
- }
63
-
64
- /**
65
- * Returns a proxy over a service whose Effect-returning methods are
66
- * transparently wrapped with `toAwaitableEffect`. Non-Effect return values
67
- * pass through untouched.
68
- */
69
- export function toAwaitableService<T extends object>(
70
- service: T,
71
- options?: AwaitableEffectOptions,
72
- ): AwaitableService<T> {
73
- const wrappedMethods = new Map<PropertyKey, Callable>()
74
-
75
- return new Proxy(service, {
76
- get(target, property, receiver) {
77
- const value = Reflect.get(target, property, receiver)
78
- if (!isCallable(value)) {
79
- return value
80
- }
81
-
82
- const existing = wrappedMethods.get(property)
83
- if (existing) {
84
- return existing
85
- }
86
-
87
- const wrapped: Callable = (...args) => {
88
- const result = value.apply(target, args)
89
- return isRuntimeEffect(result) ? toAwaitableEffect(result, options) : result
90
- }
91
-
92
- wrappedMethods.set(property, wrapped)
93
- return wrapped
94
- },
95
- }) as AwaitableService<T>
96
- }
@@ -1,25 +0,0 @@
1
- import type { ManagedRuntime } from 'effect'
2
-
3
- // eslint-disable-next-line typescript-eslint/no-explicit-any -- ManagedRuntime is contravariant in R; `any` is the only valid wildcard
4
- type SdkManagedRuntime = ManagedRuntime.ManagedRuntime<any, any>
5
-
6
- let currentRuntime: SdkManagedRuntime | null = null
7
-
8
- export function setCurrentRuntime<R, E>(runtime: ManagedRuntime.ManagedRuntime<R, E>): void {
9
- currentRuntime = runtime as SdkManagedRuntime
10
- }
11
-
12
- export function clearCurrentRuntime(): void {
13
- currentRuntime = null
14
- }
15
-
16
- export function getCurrentRuntime(): SdkManagedRuntime {
17
- if (!currentRuntime) {
18
- throw new Error('Lota Effect runtime is not configured. Call createLotaRuntime() first.')
19
- }
20
- return currentRuntime
21
- }
22
-
23
- export function getOptionalCurrentRuntime(): SdkManagedRuntime | null {
24
- return currentRuntime
25
- }
@@ -1,46 +0,0 @@
1
- import { Effect } from 'effect'
2
- import type { Context, ManagedRuntime } from 'effect'
3
-
4
- import { clearCurrentRuntime, getCurrentRuntime, getOptionalCurrentRuntime, setCurrentRuntime } from './runtime-ref'
5
-
6
- export function setLotaSdkRuntime<R, E>(runtime: ManagedRuntime.ManagedRuntime<R, E>): void {
7
- setCurrentRuntime(runtime)
8
- }
9
-
10
- export function clearLotaSdkRuntime(): void {
11
- clearCurrentRuntime()
12
- }
13
-
14
- export function getOptionalLotaSdkRuntime() {
15
- return getOptionalCurrentRuntime()
16
- }
17
-
18
- /** Returns the current managed runtime or throws. */
19
- export function getLotaSdkRuntime() {
20
- return getCurrentRuntime()
21
- }
22
-
23
- export function resolveLotaService<I, T>(tag: Context.Key<I, T>): T {
24
- return getCurrentRuntime().runSync(Effect.service(tag))
25
- }
26
-
27
- export function resolveOptionalLotaService<I, T>(tag: Context.Key<I, T>): T | null {
28
- const runtime = getOptionalCurrentRuntime()
29
- return runtime ? runtime.runSync(Effect.service(tag)) : null
30
- }
31
-
32
- export function runPromiseWithOptionalLotaRuntime<A, E>(effect: Effect.Effect<A, E, never>): Promise<A> {
33
- const runtime = getOptionalCurrentRuntime()
34
- return runtime ? runtime.runPromise(effect) : Effect.runPromise(effect)
35
- }
36
-
37
- /**
38
- * Run an effect as a Promise using the current managed runtime's context.
39
- * The runtime must be initialized via `createLotaRuntime()` before any call.
40
- */
41
- export function runPromise<A, E, R>(
42
- effect: Effect.Effect<A, E, R>,
43
- options?: { readonly signal?: AbortSignal },
44
- ): Promise<A> {
45
- return getCurrentRuntime().runPromise(effect, options)
46
- }
@@ -1,20 +0,0 @@
1
- import type IORedis from 'ioredis'
2
-
3
- import { resolveLotaService } from '../effect/runtime'
4
- import { RedisServiceTag } from '../effect/services'
5
- import type { RedisConnectionManager } from './connection'
6
-
7
- let currentRedisManager: RedisConnectionManager | null = null
8
-
9
- export function configureRuntimeRedisManager(redisManager: RedisConnectionManager): void {
10
- currentRedisManager = redisManager
11
- }
12
-
13
- export function clearRuntimeRedisManager(): void {
14
- currentRedisManager = null
15
- }
16
-
17
- export function getRuntimeRedisConnection(): IORedis {
18
- const redis = currentRedisManager ?? resolveLotaService(RedisServiceTag)
19
- return redis.getConnection()
20
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * Installs and tears down the SDK's module-level runtime accessors.
3
- *
4
- * Several SDK modules expose `configure*`/`clear*` pairs so host code can read
5
- * runtime-resolved services synchronously (AI gateway, agent defaults,
6
- * firecrawl, graph designer, queue jobs, runtime extensions, redis, thread
7
- * defaults). `createLotaRuntime` wires them once at boot and clears them on
8
- * disconnect; this module keeps both phases in a single place so they stay
9
- * symmetric.
10
- */
11
-
12
- import type { Context, ManagedRuntime } from 'effect'
13
- import { Effect } from 'effect'
14
-
15
- import {
16
- AiGatewayTag,
17
- clearAiGatewayRuntimeAccessors,
18
- configureAiGatewayRuntimeAccessors,
19
- } from '../ai-gateway/ai-gateway'
20
- import { clearAgentRuntimeDefaults, configureAgentRuntimeDefaults } from '../config/agent-defaults'
21
- import { clearThreadRuntimeDefaults, configureThreadRuntimeDefaults } from '../config/thread-defaults'
22
- import {
23
- AgentConfigServiceTag,
24
- AgentFactoryServiceTag,
25
- RuntimeAdaptersServiceTag,
26
- RuntimeWorkerExtensionsServiceTag,
27
- ThreadConfigServiceTag,
28
- ToolProvidersServiceTag,
29
- TurnHooksServiceTag,
30
- } from '../effect'
31
- import type { RedisConnectionManager } from '../redis/connection'
32
- import { clearRuntimeRedisManager, configureRuntimeRedisManager } from '../redis/runtime-connection'
33
- import { QueueJobServiceTag } from '../services/queue-job.service'
34
- import { FirecrawlTag, clearFirecrawlClient, configureFirecrawlClient } from '../tools/firecrawl-client'
35
- import { clearQueueJobService, configureQueueJobService } from '../workers/worker-utils'
36
- import { clearGraphDesigner, configureGraphDesigner } from './graph-designer'
37
- import type { ResolvedLotaRuntimeConfig } from './runtime-config'
38
- import { clearRuntimeExtensionsAccessors, configureRuntimeExtensionsAccessors } from './runtime-extensions'
39
-
40
- // eslint-disable-next-line typescript-eslint/no-explicit-any -- ManagedRuntime is contravariant in R; `any` is the only valid wildcard
41
- type SdkManagedRuntime = ManagedRuntime.ManagedRuntime<any, any>
42
-
43
- interface ConfigureRuntimeAccessorsInput {
44
- managedRuntime: SdkManagedRuntime
45
- runtimeConfig: ResolvedLotaRuntimeConfig
46
- redisManager: RedisConnectionManager
47
- }
48
-
49
- /**
50
- * Resolve every service the SDK exposes through module-level accessors and
51
- * install them. Must be called after `setLotaSdkRuntime(managedRuntime)`.
52
- */
53
- export function configureRuntimeModuleAccessors(input: ConfigureRuntimeAccessorsInput): void {
54
- const { managedRuntime, runtimeConfig, redisManager } = input
55
- const resolve = <I, T>(tag: Context.Key<I, T>): T =>
56
- // We know every tag is provided by the managed runtime; the cast bridges
57
- // the erased service-set carried by `SdkManagedRuntime`.
58
- managedRuntime.runSync(Effect.service(tag) as Effect.Effect<T, never, never>)
59
-
60
- configureAiGatewayRuntimeAccessors({ aiGateway: resolve(AiGatewayTag), runtimeConfig })
61
- configureAgentRuntimeDefaults({
62
- agentConfig: resolve(AgentConfigServiceTag),
63
- agentFactoryConfig: resolve(AgentFactoryServiceTag),
64
- })
65
- configureThreadRuntimeDefaults(resolve(ThreadConfigServiceTag))
66
- configureRuntimeExtensionsAccessors({
67
- adapters: resolve(RuntimeAdaptersServiceTag),
68
- turnHooks: resolve(TurnHooksServiceTag),
69
- toolProviders: resolve(ToolProvidersServiceTag),
70
- extraWorkers: resolve(RuntimeWorkerExtensionsServiceTag),
71
- })
72
- configureFirecrawlClient(resolve(FirecrawlTag))
73
- configureQueueJobService(resolve(QueueJobServiceTag))
74
- configureRuntimeRedisManager(redisManager)
75
- configureGraphDesigner(runtimeConfig.graphDesigner)
76
- }
77
-
78
- /**
79
- * Clears every module-level accessor installed by
80
- * `configureRuntimeModuleAccessors`. Always safe to call (idempotent per clear
81
- * function).
82
- */
83
- export function clearRuntimeModuleAccessors(): void {
84
- clearAiGatewayRuntimeAccessors()
85
- clearAgentRuntimeDefaults()
86
- clearFirecrawlClient()
87
- clearGraphDesigner()
88
- clearQueueJobService()
89
- clearRuntimeExtensionsAccessors()
90
- clearRuntimeRedisManager()
91
- clearThreadRuntimeDefaults()
92
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Single-slot runtime token. `createLotaRuntime()` claims the slot on success
3
- * and releases it on disconnect, enforcing the "one runtime per process"
4
- * invariant. Kept as a small, typed module so the guard is trivially testable
5
- * and stays out of the main entrypoint.
6
- */
7
-
8
- import { Effect } from 'effect'
9
-
10
- import { ConfigurationError } from '../effect/errors'
11
-
12
- let activeToken: symbol | null = null
13
-
14
- function buildConflictError(): ConfigurationError {
15
- return new ConfigurationError({
16
- message: 'createLotaRuntime() is process-scoped. Disconnect the active runtime before creating another one.',
17
- })
18
- }
19
-
20
- /**
21
- * Claim the process-wide runtime slot. Fails with `ConfigurationError` if it
22
- * is already held. Preferred inside `Effect.gen` so the failure is expressed
23
- * through the typed error channel instead of a thrown value.
24
- */
25
- export function acquireRuntimeTokenEffect(): Effect.Effect<symbol, ConfigurationError> {
26
- return Effect.suspend(() => {
27
- if (activeToken) {
28
- return Effect.fail(buildConflictError())
29
- }
30
-
31
- const token = Symbol('lota-runtime')
32
- activeToken = token
33
- return Effect.succeed(token)
34
- })
35
- }
36
-
37
- /** Release the slot only if the caller holds the current token. */
38
- export function releaseRuntimeToken(token: symbol): void {
39
- if (activeToken === token) {
40
- activeToken = null
41
- }
42
- }
43
-
44
- /** Diagnostic: whether a runtime is currently active in this process. */
45
- export function isRuntimeTokenActive(): boolean {
46
- return activeToken !== null
47
- }