@lota-sdk/core 0.4.27 → 0.4.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "files": [
5
5
  "src",
6
6
  "infrastructure/schema"
@@ -32,7 +32,7 @@
32
32
  "@ai-sdk/provider": "^3.0.9",
33
33
  "@chat-adapter/slack": "^4.26.0",
34
34
  "@chat-adapter/state-ioredis": "^4.26.0",
35
- "@lota-sdk/shared": "0.4.27",
35
+ "@lota-sdk/shared": "0.4.28",
36
36
  "@mendable/firecrawl-js": "^4.20.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.170",
@@ -9,6 +9,7 @@ import { ERROR_TAGS } from '../effect/errors'
9
9
  import type { TrackedBullJobLike } from '../services/queue-job.service'
10
10
  import {
11
11
  attachWorkerEvents,
12
+ attachSandboxChildRecycling,
12
13
  createTracedWorkerProcessor,
13
14
  createWorkerShutdown,
14
15
  DEFAULT_JOB_RETENTION,
@@ -42,6 +43,7 @@ interface QueueFactoryConfigBase {
42
43
  stalledInterval?: number
43
44
  maxStalledCount?: number
44
45
  defaultJobOptions?: JobsOptions
46
+ recycleSandboxChildren?: boolean
45
47
  connectionProvider: () => IORedis
46
48
  queueJobService: QueueJobService
47
49
  }
@@ -280,6 +282,9 @@ function createQueueFactoryRuntime<TJob>(config: QueueFactoryConfigBase): {
280
282
  // leaks.
281
283
  try {
282
284
  attachWorkerEvents(worker, config.displayName, logger)
285
+ if ((config.recycleSandboxChildren ?? true) && workerConfig.processorPath) {
286
+ attachSandboxChildRecycling(worker, config.displayName, logger)
287
+ }
283
288
  const shutdown = createWorkerShutdown(worker, config.displayName, logger)
284
289
 
285
290
  if (registerSignals) {
@@ -4,7 +4,7 @@ import { AiGatewayModelsTag, isAiGenerationContentFilterError } from '../ai-gate
4
4
  import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
5
5
  import type { ResolvedAgentConfig } from '../config/agent-defaults'
6
6
  import { chatLogger } from '../config/logger'
7
- import { ERROR_TAGS, ServiceError } from '../effect/errors'
7
+ import { ServiceError } from '../effect/errors'
8
8
  import { AgentConfigServiceTag } from '../effect/services'
9
9
  import type { HelperModelRuntime } from '../runtime/helper-model'
10
10
  import { HelperModelTag } from '../runtime/helper-model'
@@ -13,10 +13,19 @@ import {
13
13
  makeRecentActivityTitleRefinerAgentFactory,
14
14
  RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
15
15
  } from '../system-agents/recent-activity-title-refiner.agent'
16
+ import { compactWhitespace, truncateText } from '../utils/string'
16
17
  import type { makeRecentActivityService } from './recent-activity.service'
17
18
  import { RecentActivityServiceTag } from './recent-activity.service'
18
19
 
19
20
  const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
21
+ const RECENT_ACTIVITY_TITLE_FIELD_MAX_CHARS = 800
22
+
23
+ function formatPromptField(label: string, value: string | undefined): string | null {
24
+ if (!value) return null
25
+ const normalized = compactWhitespace(value)
26
+ if (!normalized) return null
27
+ return `${label}=${truncateText(normalized, RECENT_ACTIVITY_TITLE_FIELD_MAX_CHARS)}`
28
+ }
20
29
 
21
30
  function buildRefinementPromptInput(
22
31
  candidate: {
@@ -32,12 +41,12 @@ function buildRefinementPromptInput(
32
41
 
33
42
  const metadata = candidate.metadata
34
43
  const lines = [
35
- `sourceLabel=${candidate.sourceLabel}`,
36
- `systemTitle=${candidate.systemTitle}`,
37
- metadata.agentName ? `agentName=${metadata.agentName}` : null,
38
- metadata.threadTitle ? `threadTitle=${metadata.threadTitle}` : null,
39
- metadata.userMessageText ? `userMessage=${metadata.userMessageText}` : null,
40
- metadata.assistantSummary ? `assistantSummary=${metadata.assistantSummary}` : null,
44
+ formatPromptField('sourceLabel', candidate.sourceLabel),
45
+ formatPromptField('systemTitle', candidate.systemTitle),
46
+ formatPromptField('agentName', metadata.agentName),
47
+ formatPromptField('threadTitle', metadata.threadTitle),
48
+ formatPromptField('userMessage', metadata.userMessageText),
49
+ formatPromptField('assistantSummary', metadata.assistantSummary),
41
50
  ].filter((line): line is string => Boolean(line))
42
51
 
43
52
  if (lines.length === 0) return null
@@ -77,13 +86,12 @@ export function makeRecentActivityTitleService(
77
86
  ? cause
78
87
  : new ServiceError({ message: 'Failed to generate recent activity title refinement.', cause }),
79
88
  }).pipe(
80
- Effect.catchTag(ERROR_TAGS.AiGenerationError, (error) =>
81
- isAiGenerationContentFilterError(error)
82
- ? Effect.sync(() => {
83
- chatLogger.warn`Skipping recent activity title refinement after provider content filter (activityId=${activityId})`
84
- return null
85
- })
86
- : Effect.fail(error),
89
+ Effect.catch((error) =>
90
+ Effect.sync(() => {
91
+ const reason = isAiGenerationContentFilterError(error) ? 'provider content filter' : 'non-fatal error'
92
+ chatLogger.warn`Skipping recent activity title refinement after ${reason} (activityId=${activityId}): ${error}`
93
+ return null
94
+ }),
87
95
  ),
88
96
  )
89
97
  if (maybeRefinedTitle === null) {
@@ -95,6 +95,60 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
95
95
  }
96
96
  }
97
97
 
98
+ interface SandboxChildLike {
99
+ pid?: number
100
+ }
101
+
102
+ interface SandboxChildPoolLike {
103
+ getAllFree?: () => SandboxChildLike[]
104
+ kill?: (child: SandboxChildLike, signal?: NodeJS.Signals) => Promise<void>
105
+ }
106
+
107
+ interface SandboxedWorkerLike {
108
+ childPool?: SandboxChildPoolLike
109
+ }
110
+
111
+ function getSandboxChildPool(worker: Worker): SandboxChildPoolLike | null {
112
+ const pool = (worker as unknown as SandboxedWorkerLike).childPool
113
+ return pool && typeof pool.getAllFree === 'function' && typeof pool.kill === 'function' ? pool : null
114
+ }
115
+
116
+ export function recycleIdleSandboxChildren(
117
+ worker: Worker,
118
+ name: string,
119
+ logger: typeof chatLogger = chatLogger,
120
+ ): Promise<void> {
121
+ const pool = getSandboxChildPool(worker)
122
+ if (!pool) return Promise.resolve()
123
+
124
+ const idleChildren = pool.getAllFree?.() ?? []
125
+ if (idleChildren.length === 0) return Promise.resolve()
126
+
127
+ return Promise.all(
128
+ idleChildren.map((child) =>
129
+ (pool.kill?.(child, 'SIGTERM') ?? Promise.resolve()).catch((error: unknown) => {
130
+ logger.warn`Failed to recycle idle ${name} sandbox child (${child.pid ?? 'unknown'}): ${error}`
131
+ }),
132
+ ),
133
+ ).then(() => undefined)
134
+ }
135
+
136
+ export function attachSandboxChildRecycling(
137
+ worker: Worker,
138
+ name: string,
139
+ logger: typeof chatLogger = chatLogger,
140
+ ): void {
141
+ const recycle = () => {
142
+ // @effect-diagnostics-next-line globalTimers:off -- BullMQ worker event callback; defer until BullMQ releases the child.
143
+ setTimeout(() => {
144
+ void recycleIdleSandboxChildren(worker, name, logger)
145
+ }, 0)
146
+ }
147
+
148
+ worker.on('completed', recycle)
149
+ worker.on('failed', recycle)
150
+ }
151
+
98
152
  export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TResult = void>(
99
153
  queueName: string,
100
154
  processor: (job: TJob) => Promise<TResult>,