@lota-sdk/core 0.4.26 → 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.26",
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.26",
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) {
@@ -3,6 +3,7 @@ import type { ChatMessage } from '@lota-sdk/shared'
3
3
 
4
4
  import type { ResolvedAgentConfig } from '../config/agent-defaults'
5
5
  import { resolveAgentNameAlias } from '../config/agent-defaults'
6
+ import { truncateText } from '../utils/string'
6
7
  import type { ChatMessageLike, ReadableUploadMetadataLike } from './chat-types'
7
8
 
8
9
  export interface ThreadHistoryMessage {
@@ -11,6 +12,9 @@ export interface ThreadHistoryMessage {
11
12
  agentName?: string
12
13
  }
13
14
 
15
+ const THREAD_HISTORY_MESSAGE_MAX_CHARS = 4_000
16
+ const CONVERSATION_SUMMARY_MAX_CHARS = 12_000
17
+
14
18
  export function asRecord(value: unknown): Record<string, unknown> | null {
15
19
  return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
16
20
  }
@@ -53,7 +57,7 @@ export function toHistoryMessages(
53
57
  ): ThreadHistoryMessage[] {
54
58
  return messages
55
59
  .map((message): ThreadHistoryMessage | null => {
56
- const content = extractMessageText(message)
60
+ const content = truncateText(extractMessageText(message), THREAD_HISTORY_MESSAGE_MAX_CHARS)
57
61
  if (!content) return null
58
62
 
59
63
  if (message.role === 'user') {
@@ -75,15 +79,28 @@ export function buildConversationSummary(params: {
75
79
  assistantMessages: ChatMessageLike[]
76
80
  }): string {
77
81
  const lines: string[] = []
82
+ let summaryChars = 0
83
+
84
+ const appendLine = (line: string): boolean => {
85
+ const separatorChars = lines.length === 0 ? 0 : 2
86
+ const remaining = CONVERSATION_SUMMARY_MAX_CHARS - summaryChars - separatorChars
87
+ if (remaining <= 3) return false
88
+
89
+ const nextLine = line.length > remaining ? truncateText(line, remaining) : line
90
+ lines.push(nextLine)
91
+ summaryChars += separatorChars + nextLine.length
92
+ return line.length <= remaining
93
+ }
94
+
78
95
  if (params.userMessageText.trim()) {
79
- lines.push(`User: ${params.userMessageText.trim()}`)
96
+ appendLine(`User: ${truncateText(params.userMessageText.trim(), THREAD_HISTORY_MESSAGE_MAX_CHARS)}`)
80
97
  }
81
98
 
82
99
  for (const message of params.assistantMessages) {
83
- const content = extractMessageText(message)
100
+ const content = truncateText(extractMessageText(message), THREAD_HISTORY_MESSAGE_MAX_CHARS)
84
101
  if (!content) continue
85
102
  const agentName = getAgentName(params.agentConfig, message)
86
- lines.push(agentName ? `${agentName}: ${content}` : `Assistant: ${content}`)
103
+ if (!appendLine(agentName ? `${agentName}: ${content}` : `Assistant: ${content}`)) break
87
104
  }
88
105
 
89
106
  return lines.join('\n\n').trim()
@@ -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) {
@@ -191,14 +191,19 @@ export function makeSocialChatHistoryService(
191
191
  params.cursor?.createdAt.getTime() ??
192
192
  (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
193
193
  const limit = typeof params.limit === 'number' ? Math.max(1, Math.trunc(params.limit)) : undefined
194
- const rangeLimitArgs = limit === undefined ? [] : (['LIMIT', 0, limit + 1] as const)
195
194
  const storageKeys = yield* Effect.tryPromise({
196
- try: () =>
197
- params.cursor || params.onboardingCutoff
198
- ? conn.zrangebyscore(indexKey, scoreStart, '+inf', ...rangeLimitArgs)
199
- : limit === undefined
200
- ? conn.zrange(indexKey, 0, -1)
201
- : conn.zrange(indexKey, 0, limit - 1),
195
+ try: () => {
196
+ if (params.cursor || params.onboardingCutoff) {
197
+ if (limit === undefined) {
198
+ return conn.zrangebyscore(indexKey, scoreStart, '+inf')
199
+ }
200
+ return conn
201
+ .call('ZRANGEBYSCORE', indexKey, String(scoreStart), '+inf', 'LIMIT', '0', String(limit + 1))
202
+ .then((result) => (Array.isArray(result) ? result.map(String) : []))
203
+ }
204
+
205
+ return limit === undefined ? conn.zrange(indexKey, 0, -1) : conn.zrange(indexKey, 0, limit - 1)
206
+ },
202
207
  catch: (cause) => new SocialChatHistoryError({ message: 'Failed to list workspace message keys.', cause }),
203
208
  })
204
209
  if (storageKeys.length === 0) return [] as SocialChatHistoryMessage[]
@@ -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>,