@lota-sdk/core 0.4.13 → 0.4.15
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 +4 -4
- package/src/ai/embedding-cache.ts +17 -11
- package/src/ai-gateway/ai-gateway.ts +164 -94
- package/src/ai-gateway/index.ts +4 -1
- package/src/config/agent-defaults.ts +2 -2
- package/src/config/agent-types.ts +1 -1
- package/src/config/constants.ts +1 -1
- package/src/create-runtime.ts +259 -200
- package/src/db/cursor-pagination.ts +2 -9
- package/src/db/memory-store.ts +194 -175
- package/src/db/memory.ts +125 -71
- package/src/db/schema-fingerprint.ts +5 -4
- package/src/db/service-normalization.ts +4 -3
- package/src/db/service.ts +3 -2
- package/src/db/startup.ts +15 -16
- package/src/effect/errors.ts +161 -21
- package/src/effect/index.ts +0 -1
- package/src/embeddings/provider.ts +15 -7
- package/src/queues/autonomous-job.queue.ts +10 -22
- package/src/queues/delayed-node-promotion.queue.ts +8 -14
- package/src/queues/document-processor.queue.ts +13 -4
- package/src/queues/memory-consolidation.queue.ts +26 -14
- package/src/queues/plan-agent-heartbeat.queue.ts +48 -31
- package/src/queues/plan-scheduler.queue.ts +37 -15
- package/src/queues/queue-factory.ts +59 -35
- package/src/queues/standalone-worker.ts +3 -2
- package/src/redis/connection.ts +10 -3
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +5 -5
- package/src/redis/stream-context.ts +1 -1
- package/src/runtime/chat-message.ts +64 -1
- package/src/runtime/chat-run-orchestration.ts +33 -20
- package/src/runtime/context-compaction/context-compaction-runtime.ts +14 -7
- package/src/runtime/context-compaction/context-compaction.ts +78 -66
- package/src/runtime/domain-layer.ts +19 -13
- package/src/runtime/execution-plan.ts +7 -3
- package/src/runtime/memory/memory-block.ts +3 -9
- package/src/runtime/memory/memory-scope.ts +3 -1
- package/src/runtime/plugin-resolution.ts +2 -1
- package/src/runtime/post-turn-side-effects.ts +6 -5
- package/src/runtime/retrieval-adapters.ts +8 -20
- package/src/runtime/runtime-config.ts +3 -9
- package/src/runtime/runtime-extensions.ts +2 -4
- package/src/runtime/runtime-lifecycle.ts +56 -16
- package/src/runtime/runtime-services.ts +180 -102
- package/src/runtime/runtime-worker-registry.ts +3 -1
- package/src/runtime/social-chat/social-chat-agent-runner.ts +1 -1
- package/src/runtime/social-chat/social-chat-history.ts +21 -18
- package/src/runtime/social-chat/social-chat.ts +356 -223
- package/src/runtime/specialist-runner.ts +3 -1
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +3 -2
- package/src/runtime/thread-turn-context.ts +142 -102
- package/src/runtime/turn-lifecycle.ts +15 -46
- package/src/services/agent-activity.service.ts +1 -1
- package/src/services/agent-executor.service.ts +107 -77
- package/src/services/autonomous-job.service.ts +354 -293
- package/src/services/background-work.service.ts +3 -3
- package/src/services/context-compaction.service.ts +7 -2
- package/src/services/document-chunk.service.ts +50 -32
- package/src/services/execution-plan/execution-plan-schedule.ts +5 -3
- package/src/services/execution-plan/execution-plan.service.ts +162 -179
- package/src/services/feedback-loop.service.ts +5 -4
- package/src/services/graph-full-routing.ts +37 -36
- package/src/services/institutional-memory.service.ts +28 -30
- package/src/services/learned-skill.service.ts +107 -72
- package/src/services/memory/memory-errors.ts +4 -23
- package/src/services/memory/memory-org-memory.ts +10 -5
- package/src/services/memory/memory-rerank.ts +18 -6
- package/src/services/memory/memory.service.ts +170 -111
- package/src/services/memory/rerank.service.ts +29 -20
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +69 -75
- package/src/services/ownership-dispatcher.service.ts +40 -39
- package/src/services/plan/plan-agent-heartbeat.service.ts +22 -24
- package/src/services/plan/plan-agent-query.service.ts +39 -31
- package/src/services/plan/plan-completion-side-effects.ts +13 -17
- package/src/services/plan/plan-coordination.service.ts +2 -1
- package/src/services/plan/plan-cycle.service.ts +6 -5
- package/src/services/plan/plan-deadline.service.ts +57 -54
- package/src/services/plan/plan-event-delivery.service.ts +5 -4
- package/src/services/plan/plan-executor-graph.ts +18 -15
- package/src/services/plan/plan-executor.service.ts +235 -262
- package/src/services/plan/plan-run.service.ts +169 -93
- package/src/services/plan/plan-scheduler.service.ts +192 -202
- package/src/services/plan/plan-template.service.ts +1 -1
- package/src/services/plan/plan-transaction-events.ts +1 -1
- package/src/services/plan/plan-workspace.service.ts +23 -14
- package/src/services/plugin-executor.service.ts +5 -9
- package/src/services/queue-job.service.ts +117 -59
- package/src/services/recent-activity-title.service.ts +13 -12
- package/src/services/recent-activity.service.ts +6 -1
- package/src/services/social-chat-history.service.ts +29 -25
- package/src/services/system-executor.service.ts +5 -9
- package/src/services/thread/thread-active-run.ts +2 -2
- package/src/services/thread/thread-listing.ts +61 -57
- package/src/services/thread/thread-memory-block.ts +73 -48
- package/src/services/thread/thread-message.service.ts +76 -65
- package/src/services/thread/thread-record-store.ts +8 -8
- package/src/services/thread/thread-title.service.ts +10 -4
- package/src/services/thread/thread-turn-execution.ts +43 -45
- package/src/services/thread/thread-turn-preparation.service.ts +257 -135
- package/src/services/thread/thread-turn-streaming.ts +83 -92
- package/src/services/thread/thread-turn.ts +18 -16
- package/src/services/thread/thread.service.ts +135 -100
- package/src/services/user.service.ts +45 -48
- package/src/storage/attachment-parser.ts +6 -2
- package/src/storage/attachment-storage.service.ts +5 -6
- package/src/storage/generated-document-storage.service.ts +1 -1
- package/src/system-agents/context-compaction.agent.ts +10 -9
- package/src/system-agents/delegated-agent-factory.ts +30 -6
- package/src/system-agents/memory-reranker.agent.ts +10 -9
- package/src/system-agents/memory.agent.ts +10 -9
- package/src/system-agents/recent-activity-title-refiner.agent.ts +13 -15
- package/src/system-agents/regular-chat-memory-digest.agent.ts +13 -12
- package/src/system-agents/skill-extractor.agent.ts +13 -12
- package/src/system-agents/skill-manager.agent.ts +13 -12
- package/src/system-agents/thread-router.agent.ts +11 -7
- package/src/system-agents/title-generator.agent.ts +13 -12
- package/src/tools/fetch-webpage.tool.ts +13 -13
- package/src/tools/memory-block.tool.ts +3 -1
- package/src/tools/plan-approval.tool.ts +4 -2
- package/src/tools/read-file-parts.tool.ts +10 -4
- package/src/tools/remember-memory.tool.ts +3 -1
- package/src/tools/research-topic.tool.ts +9 -5
- package/src/tools/search-web.tool.ts +16 -16
- package/src/tools/search.tool.ts +20 -5
- package/src/tools/team-think.tool.ts +61 -38
- package/src/utils/async.ts +5 -5
- package/src/utils/errors.ts +19 -18
- package/src/utils/sse-keepalive.ts +28 -25
- package/src/workers/bootstrap.ts +75 -11
- package/src/workers/memory-consolidation.worker.ts +82 -91
- package/src/workers/organization-learning.worker.ts +14 -4
- package/src/workers/regular-chat-memory-digest.runner.ts +105 -67
- package/src/workers/skill-extraction.runner.ts +97 -61
- package/src/workers/utils/repo-structure-extractor.ts +13 -8
- package/src/workers/utils/thread-message-query.ts +24 -24
- package/src/workers/worker-utils.ts +23 -4
- package/src/effect/helpers.ts +0 -123
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type Firecrawl from '@mendable/firecrawl-js'
|
|
2
|
+
import type { Effect } from 'effect'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import type { AiGatewayModels } from '../ai-gateway/ai-gateway'
|
|
4
5
|
import { buildAiGatewayStrictSemanticCacheHeaders } from '../ai-gateway/cache-headers'
|
|
5
6
|
import {
|
|
6
7
|
OPENROUTER_FAST_REASONING_MODEL_ID,
|
|
@@ -13,19 +14,22 @@ import { searchWebTool } from './search-web.tool'
|
|
|
13
14
|
|
|
14
15
|
export interface ResearchTopicToolContext {
|
|
15
16
|
firecrawl: Firecrawl
|
|
17
|
+
aiGatewayModels: AiGatewayModels
|
|
18
|
+
runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export const researchTopicTool = createDelegatedAgentToolWithContext<ResearchTopicToolContext>({
|
|
19
22
|
id: 'researchTopic',
|
|
20
23
|
description:
|
|
21
24
|
'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report.',
|
|
22
|
-
model: () =>
|
|
25
|
+
model: ({ aiGatewayModels }) => aiGatewayModels.chatModel(OPENROUTER_FAST_REASONING_MODEL_ID),
|
|
23
26
|
providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
24
27
|
headers: buildAiGatewayStrictSemanticCacheHeaders('researchTopic'),
|
|
25
28
|
instructions: RESEARCHER_PROMPT,
|
|
26
|
-
createTools: ({ firecrawl }) => ({
|
|
27
|
-
searchWeb: searchWebTool.create({ firecrawl }),
|
|
28
|
-
fetchWebpage: fetchWebpageTool.create({ firecrawl }),
|
|
29
|
+
createTools: ({ firecrawl, runPromise }) => ({
|
|
30
|
+
searchWeb: searchWebTool.create({ firecrawl, runPromise }),
|
|
31
|
+
fetchWebpage: fetchWebpageTool.create({ firecrawl, runPromise }),
|
|
29
32
|
}),
|
|
30
33
|
maxSteps: 6,
|
|
34
|
+
getRunPromise: (context) => context.runPromise,
|
|
31
35
|
})
|
|
@@ -4,6 +4,7 @@ import { Effect, Schema } from 'effect'
|
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
6
|
import type { ToolDefinition } from '../ai/definitions'
|
|
7
|
+
import { ERROR_TAGS } from '../effect/errors'
|
|
7
8
|
import { withTimeout } from '../utils/async'
|
|
8
9
|
import { nowIsoDateTimeString } from '../utils/date-time'
|
|
9
10
|
import { readStringField, truncateOptionalText } from '../utils/string'
|
|
@@ -12,9 +13,10 @@ import { toRecord, WEB_TOOL_TIMEOUT_MS } from './web-tool-shared'
|
|
|
12
13
|
|
|
13
14
|
export interface SearchWebToolContext {
|
|
14
15
|
firecrawl: Firecrawl
|
|
16
|
+
runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
class SearchWebToolError extends Schema.TaggedErrorClass<SearchWebToolError>()(
|
|
19
|
+
class SearchWebToolError extends Schema.TaggedErrorClass<SearchWebToolError>()(ERROR_TAGS.SearchWebToolError, {
|
|
18
20
|
message: Schema.String,
|
|
19
21
|
cause: Schema.optional(Schema.Defect),
|
|
20
22
|
}) {}
|
|
@@ -139,7 +141,7 @@ function buildWebCitations(results: { web?: unknown[]; news?: unknown[]; images?
|
|
|
139
141
|
|
|
140
142
|
export const searchWebTool = {
|
|
141
143
|
name: 'searchWeb',
|
|
142
|
-
create: ({ firecrawl }: SearchWebToolContext) =>
|
|
144
|
+
create: ({ firecrawl, runPromise }: SearchWebToolContext) =>
|
|
143
145
|
tool({
|
|
144
146
|
description: 'Search the web for real-time information.',
|
|
145
147
|
inputSchema: z
|
|
@@ -151,20 +153,17 @@ export const searchWebTool = {
|
|
|
151
153
|
tbs: z.string().optional(),
|
|
152
154
|
})
|
|
153
155
|
.strict(),
|
|
154
|
-
execute: (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
query: string
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
tbs?: string
|
|
166
|
-
}) =>
|
|
167
|
-
Effect.runPromise(
|
|
156
|
+
execute: (
|
|
157
|
+
{
|
|
158
|
+
query,
|
|
159
|
+
limit,
|
|
160
|
+
sources,
|
|
161
|
+
location,
|
|
162
|
+
tbs,
|
|
163
|
+
}: { query: string; limit?: number; sources?: ('web' | 'news' | 'images')[]; location?: string; tbs?: string },
|
|
164
|
+
{ abortSignal }: { abortSignal?: AbortSignal } = {},
|
|
165
|
+
) =>
|
|
166
|
+
runPromise(
|
|
168
167
|
Effect.gen(function* () {
|
|
169
168
|
const results = yield* Effect.tryPromise({
|
|
170
169
|
try: () =>
|
|
@@ -185,6 +184,7 @@ export const searchWebTool = {
|
|
|
185
184
|
citations: buildWebCitations(results),
|
|
186
185
|
}
|
|
187
186
|
}).pipe(Effect.withSpan('tool.searchWeb.execute')),
|
|
187
|
+
abortSignal ? { signal: abortSignal } : undefined,
|
|
188
188
|
),
|
|
189
189
|
}),
|
|
190
190
|
} as const satisfies ToolDefinition<SearchWebToolContext>
|
package/src/tools/search.tool.ts
CHANGED
|
@@ -15,18 +15,24 @@ const ConversationSearchInputSchema = z.object({ query: z.string().min(1), type:
|
|
|
15
15
|
type MemorySearchService = Pick<ReturnType<typeof createMemoryService>, 'searchAllMemoriesBatched'>
|
|
16
16
|
type ConversationSearchService = Pick<ReturnType<typeof makeThreadMessageService>, 'searchMessagesEffect'>
|
|
17
17
|
|
|
18
|
+
type SearchToolRunPromise = <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
|
|
19
|
+
|
|
18
20
|
export function createMemorySearchTool(
|
|
19
21
|
agentConfig: ResolvedAgentConfig,
|
|
20
22
|
orgIdString: string,
|
|
21
23
|
memoryService: MemorySearchService,
|
|
24
|
+
runPromise: SearchToolRunPromise,
|
|
22
25
|
agentName?: string,
|
|
23
26
|
options?: { fastMode?: boolean; allowMultiScopeRerank?: boolean },
|
|
24
27
|
) {
|
|
25
28
|
return tool({
|
|
26
29
|
description: 'Search organization and agent memories relevant to a query.',
|
|
27
30
|
inputSchema: MemorySearchInputSchema,
|
|
28
|
-
execute: (
|
|
29
|
-
|
|
31
|
+
execute: (
|
|
32
|
+
{ query }: z.infer<typeof MemorySearchInputSchema>,
|
|
33
|
+
{ abortSignal }: { abortSignal?: AbortSignal } = {},
|
|
34
|
+
) =>
|
|
35
|
+
runPromise(
|
|
30
36
|
Effect.gen(function* () {
|
|
31
37
|
const normalizedQuery = query.trim()
|
|
32
38
|
const retrieval = yield* memoryService.searchAllMemoriesBatched({
|
|
@@ -41,16 +47,24 @@ export function createMemorySearchTool(
|
|
|
41
47
|
const reminder = `Remember: you searched for "${normalizedQuery}". Use the highest-relevance retrieved lines above to answer.`
|
|
42
48
|
return { query: normalizedQuery, retrieval, reminder }
|
|
43
49
|
}),
|
|
50
|
+
abortSignal ? { signal: abortSignal } : undefined,
|
|
44
51
|
),
|
|
45
52
|
})
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
export function createConversationSearchTool(
|
|
55
|
+
export function createConversationSearchTool(
|
|
56
|
+
threadId: RecordIdRef,
|
|
57
|
+
threadMessageService: ConversationSearchService,
|
|
58
|
+
runPromise: SearchToolRunPromise,
|
|
59
|
+
) {
|
|
49
60
|
return tool({
|
|
50
61
|
description: 'Search prior chat messages by role and query text.',
|
|
51
62
|
inputSchema: ConversationSearchInputSchema,
|
|
52
|
-
execute: (
|
|
53
|
-
|
|
63
|
+
execute: (
|
|
64
|
+
{ query, type }: z.infer<typeof ConversationSearchInputSchema>,
|
|
65
|
+
{ abortSignal }: { abortSignal?: AbortSignal } = {},
|
|
66
|
+
) =>
|
|
67
|
+
runPromise(
|
|
54
68
|
Effect.gen(function* () {
|
|
55
69
|
const normalizedQuery = query.trim()
|
|
56
70
|
const results = yield* threadMessageService.searchMessagesEffect({
|
|
@@ -61,6 +75,7 @@ export function createConversationSearchTool(threadId: RecordIdRef, threadMessag
|
|
|
61
75
|
})
|
|
62
76
|
return { query: normalizedQuery, type, count: results.length, results }
|
|
63
77
|
}),
|
|
78
|
+
abortSignal ? { signal: abortSignal } : undefined,
|
|
64
79
|
),
|
|
65
80
|
})
|
|
66
81
|
}
|
|
@@ -7,7 +7,7 @@ import { aiLogger } from '../config/logger'
|
|
|
7
7
|
import type { RecordIdRef } from '../db/record-id'
|
|
8
8
|
import { recordIdToString } from '../db/record-id'
|
|
9
9
|
import { TABLES } from '../db/tables'
|
|
10
|
-
import {
|
|
10
|
+
import { ERROR_TAGS } from '../effect/errors'
|
|
11
11
|
import {
|
|
12
12
|
AgentConfigServiceTag,
|
|
13
13
|
AgentFactoryServiceTag,
|
|
@@ -35,29 +35,25 @@ function buildTeamThinkAgentToolsEffect(
|
|
|
35
35
|
return Effect.succeed({ tools: {} })
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
return
|
|
38
|
+
return Effect.tryPromise({
|
|
39
|
+
try: () => builder(params),
|
|
40
|
+
catch: (error) => new TeamThinkRuntimeError({ message: 'Failed to build team-think agent tools.', cause: error }),
|
|
41
|
+
})
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
const TEAM_THINK_AGENT_MAX_RETRIES = 1
|
|
42
45
|
const TEAM_THINK_AGENT_MAX_STEPS = 3
|
|
43
46
|
|
|
44
47
|
class TeamThinkAgentFactoryNotConfiguredError extends Schema.TaggedErrorClass<TeamThinkAgentFactoryNotConfiguredError>()(
|
|
45
|
-
'TeamThinkAgentFactoryNotConfiguredError',
|
|
48
|
+
'@lota-sdk/core/TeamThinkAgentFactoryNotConfiguredError',
|
|
46
49
|
{ agentId: Schema.String },
|
|
47
50
|
) {}
|
|
48
51
|
|
|
49
|
-
class TeamThinkRuntimeError extends Schema.TaggedErrorClass<TeamThinkRuntimeError>()(
|
|
52
|
+
class TeamThinkRuntimeError extends Schema.TaggedErrorClass<TeamThinkRuntimeError>()(ERROR_TAGS.TeamThinkRuntimeError, {
|
|
50
53
|
message: Schema.String,
|
|
51
54
|
cause: Schema.optional(Schema.Defect),
|
|
52
55
|
}) {}
|
|
53
56
|
|
|
54
|
-
function effectTryMaybeAsync<A>(
|
|
55
|
-
evaluate: () => A | PromiseLike<A>,
|
|
56
|
-
message: string,
|
|
57
|
-
): Effect.Effect<A, TeamThinkRuntimeError> {
|
|
58
|
-
return effectTryMaybeAsyncShared(evaluate, (error) => new TeamThinkRuntimeError({ message, cause: error }))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
57
|
export function createTeamThinkTool(params: {
|
|
62
58
|
historyMessages: ChatMessage[]
|
|
63
59
|
latestUserMessageId: string
|
|
@@ -76,6 +72,7 @@ export function createTeamThinkTool(params: {
|
|
|
76
72
|
context?: unknown
|
|
77
73
|
toolProviders?: ToolSet
|
|
78
74
|
abortSignal: AbortSignal
|
|
75
|
+
runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: { signal?: AbortSignal }) => Promise<A>
|
|
79
76
|
}) {
|
|
80
77
|
return Effect.gen(function* () {
|
|
81
78
|
const turnHooks = yield* TurnHooksServiceTag
|
|
@@ -87,35 +84,54 @@ export function createTeamThinkTool(params: {
|
|
|
87
84
|
)
|
|
88
85
|
const participantRunner: TeamConsultationParticipantRunner = {
|
|
89
86
|
buildParticipantAgent(agentId, runParams) {
|
|
90
|
-
return
|
|
87
|
+
return params.runPromise(
|
|
91
88
|
Effect.gen(function* () {
|
|
89
|
+
// Capture the fiber context of the managed runtime and replay it
|
|
90
|
+
// for observer callbacks that cross the Effect → Promise boundary
|
|
91
|
+
// (AI SDK agent observers). This preserves spans/loggers across
|
|
92
|
+
// the callback edge — it is NOT an ambient-runtime slot; the
|
|
93
|
+
// captured context lives only for this participant's fiber.
|
|
92
94
|
const currentContext = yield* Effect.context()
|
|
93
95
|
const runPromiseWithCurrentContext = Effect.runPromiseWith(currentContext)
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const getAdditionalInstructionSections = params.getAdditionalInstructionSections
|
|
97
|
+
const dynamicInstructionSections = getAdditionalInstructionSections
|
|
98
|
+
? yield* Effect.tryPromise({
|
|
99
|
+
try: () => getAdditionalInstructionSections(),
|
|
100
|
+
catch: (error) =>
|
|
101
|
+
new TeamThinkRuntimeError({
|
|
102
|
+
message: 'Failed to load dynamic team-think instruction sections.',
|
|
103
|
+
cause: error,
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
: undefined
|
|
107
|
+
const resolveAgent = turnHooks.resolveAgent
|
|
98
108
|
const agentResolution = asRecord(
|
|
99
|
-
|
|
100
|
-
(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
resolveAgent
|
|
110
|
+
? yield* Effect.tryPromise({
|
|
111
|
+
try: () =>
|
|
112
|
+
resolveAgent({
|
|
113
|
+
agentId,
|
|
114
|
+
mode: 'fixedThreadMode',
|
|
115
|
+
thread: null,
|
|
116
|
+
threadRef: params.threadId,
|
|
117
|
+
orgRef: params.orgId,
|
|
118
|
+
userRef: params.userId,
|
|
119
|
+
onboardingActive: false,
|
|
120
|
+
linearInstalled: false,
|
|
121
|
+
githubInstalled: params.githubInstalled,
|
|
122
|
+
additionalInstructionSections: mergeInstructionSections(
|
|
123
|
+
dynamicInstructionSections,
|
|
124
|
+
params.additionalInstructionSections,
|
|
125
|
+
),
|
|
126
|
+
context: (params.context as Record<string, unknown> | null | undefined) ?? null,
|
|
127
|
+
}),
|
|
128
|
+
catch: (error) =>
|
|
129
|
+
new TeamThinkRuntimeError({
|
|
130
|
+
message: 'Failed to resolve team-think participant agent.',
|
|
131
|
+
cause: error,
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
: undefined,
|
|
119
135
|
)
|
|
120
136
|
const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? agentId
|
|
121
137
|
const config = agentFactoryConfig.getAgentRuntimeConfig({
|
|
@@ -169,9 +185,16 @@ export function createTeamThinkTool(params: {
|
|
|
169
185
|
stopWhen: [stepCountIs(maxSteps)],
|
|
170
186
|
})
|
|
171
187
|
const observer = {
|
|
172
|
-
run: <T>(fn: () =>
|
|
188
|
+
run: <T>(fn: () => Promise<T>): Promise<T> =>
|
|
173
189
|
runPromiseWithCurrentContext(
|
|
174
|
-
|
|
190
|
+
Effect.tryPromise({
|
|
191
|
+
try: () => fn(),
|
|
192
|
+
catch: (error) =>
|
|
193
|
+
new TeamThinkRuntimeError({
|
|
194
|
+
message: `Team-think participant run failed (${agentId}).`,
|
|
195
|
+
cause: error,
|
|
196
|
+
}),
|
|
197
|
+
}),
|
|
175
198
|
),
|
|
176
199
|
recordError: (error: unknown) => {
|
|
177
200
|
aiLogger.error`Team-think participant failed (${agentId}): ${error}`
|
package/src/utils/async.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Cause, Duration, Effect, Exit, Schema } from 'effect'
|
|
2
2
|
|
|
3
3
|
import { serverLogger } from '../config/logger'
|
|
4
|
-
import { TimeoutError } from '../effect/errors'
|
|
4
|
+
import { ERROR_TAGS, TimeoutError } from '../effect/errors'
|
|
5
5
|
import { getErrorMessage, toError } from './errors'
|
|
6
6
|
|
|
7
|
-
class TimedOperationError extends Schema.TaggedErrorClass<TimedOperationError>()(
|
|
7
|
+
class TimedOperationError extends Schema.TaggedErrorClass<TimedOperationError>()(ERROR_TAGS.TimedOperationError, {
|
|
8
8
|
operation: Schema.String,
|
|
9
9
|
cause: Schema.Defect,
|
|
10
10
|
}) {}
|
|
11
11
|
|
|
12
12
|
function isTimedOperationError(error: unknown): error is TimedOperationError {
|
|
13
|
-
return typeof error === 'object' && error !== null && '_tag' in error && error._tag ===
|
|
13
|
+
return typeof error === 'object' && error !== null && '_tag' in error && error._tag === ERROR_TAGS.TimedOperationError
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
|
|
@@ -46,8 +46,8 @@ export function createSafeEnqueue(logger: { warn: (message: string) => void }) {
|
|
|
46
46
|
|
|
47
47
|
const _defaultSafeEnqueue = createSafeEnqueue({ warn: (message: string) => serverLogger.warn`${message}` })
|
|
48
48
|
export function safeEnqueue<T>(
|
|
49
|
-
operation: () =>
|
|
49
|
+
operation: () => Promise<T>,
|
|
50
50
|
options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
|
|
51
51
|
): Promise<T | void> {
|
|
52
|
-
return _defaultSafeEnqueue(
|
|
52
|
+
return _defaultSafeEnqueue(operation, options)
|
|
53
53
|
}
|
package/src/utils/errors.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { getErrorMessage } from '@lota-sdk/shared'
|
|
2
2
|
import { Data, Match } from 'effect'
|
|
3
3
|
|
|
4
|
-
import type { EffectError, ValidationIssue } from '../effect/errors'
|
|
5
|
-
import {
|
|
4
|
+
import type { BaseServicePersistenceError, EffectError, ValidationIssue } from '../effect/errors'
|
|
5
|
+
import { ERROR_TAGS, isEffectError } from '../effect/errors'
|
|
6
6
|
|
|
7
7
|
export function toError(value: unknown): Error {
|
|
8
8
|
return value instanceof Error ? value : new Error(String(value))
|
|
@@ -129,29 +129,30 @@ export function toAppErrorResponse(error: AppErrorLike): AppErrorResponse {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function isBaseServicePersistenceError(error: EffectError): error is BaseServicePersistenceError {
|
|
132
|
-
return error._tag === BaseServicePersistenceError
|
|
132
|
+
return error._tag === ERROR_TAGS.BaseServicePersistenceError
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const toHttpErrorMatch = Match.type<Exclude<EffectError, BaseServicePersistenceError>>().pipe(
|
|
136
|
-
Match.tag(
|
|
137
|
-
Match.tag(
|
|
138
|
-
Match.tag(
|
|
139
|
-
Match.tag(
|
|
140
|
-
Match.tag(
|
|
141
|
-
Match.tag(
|
|
136
|
+
Match.tag(ERROR_TAGS.NotFoundError, (e) => httpError(e.message, 'NOT_FOUND', 404)),
|
|
137
|
+
Match.tag(ERROR_TAGS.BadRequestError, (e) => httpError(e.message, 'BAD_REQUEST', 400)),
|
|
138
|
+
Match.tag(ERROR_TAGS.ValidationError, (e) => httpError(e.message, 'VALIDATION_ERROR', 400)),
|
|
139
|
+
Match.tag(ERROR_TAGS.ConflictError, (e) => httpError(e.message, 'CONFLICT', 409)),
|
|
140
|
+
Match.tag(ERROR_TAGS.ForbiddenError, (e) => httpError(e.message, 'FORBIDDEN', 403)),
|
|
141
|
+
Match.tag(ERROR_TAGS.ThreadTurnError, (e) =>
|
|
142
142
|
httpError(e.message, e.reason === 'conflict' ? 'CONFLICT' : 'BAD_REQUEST', e.reason === 'conflict' ? 409 : 400),
|
|
143
143
|
),
|
|
144
|
-
Match.tag(
|
|
145
|
-
Match.tag(
|
|
146
|
-
Match.tag(
|
|
147
|
-
Match.tag(
|
|
148
|
-
Match.tag(
|
|
144
|
+
Match.tag(ERROR_TAGS.ActiveThreadRunConflictError, (e) => httpError(e.message, 'CONFLICT', 409)),
|
|
145
|
+
Match.tag(ERROR_TAGS.ConfigurationError, (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
|
|
146
|
+
Match.tag(ERROR_TAGS.DatabaseError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
147
|
+
Match.tag(ERROR_TAGS.RedisError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
148
|
+
Match.tag(ERROR_TAGS.RuntimeLifecycleError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
149
|
+
Match.tag(ERROR_TAGS.TimeoutError, (e) =>
|
|
149
150
|
httpError(`Operation "${e.operation}" timed out after ${e.ms}ms`, 'INTERNAL_SERVER_ERROR', 500),
|
|
150
151
|
),
|
|
151
|
-
Match.tag(
|
|
152
|
-
Match.tag(
|
|
153
|
-
Match.tag(
|
|
154
|
-
Match.tag(
|
|
152
|
+
Match.tag(ERROR_TAGS.LockAcquisitionError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
153
|
+
Match.tag(ERROR_TAGS.LockLostError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
154
|
+
Match.tag(ERROR_TAGS.AiGenerationError, (e) => httpError(e.message, 'INTERNAL_SERVER_ERROR', 500)),
|
|
155
|
+
Match.tag(ERROR_TAGS.ServiceError, () => httpError('Internal server error', 'INTERNAL_SERVER_ERROR', 500)),
|
|
155
156
|
Match.exhaustive,
|
|
156
157
|
)
|
|
157
158
|
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Fiber } from 'effect'
|
|
2
|
+
import { Duration, Effect, Exit, Schema, Scope } from 'effect'
|
|
3
|
+
|
|
4
|
+
import { ERROR_TAGS } from '../effect/errors'
|
|
2
5
|
|
|
3
6
|
const KEEPALIVE_COMMENT = new TextEncoder().encode(': keepalive\n\n')
|
|
4
7
|
const DEFAULT_KEEPALIVE_INTERVAL_MS = 20_000
|
|
5
8
|
|
|
9
|
+
class SseKeepaliveError extends Schema.TaggedErrorClass<SseKeepaliveError>()(ERROR_TAGS.SseKeepaliveError, {
|
|
10
|
+
phase: Schema.Literals(['read']),
|
|
11
|
+
cause: Schema.Defect,
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Wraps an SSE Response body with periodic keepalive comments.
|
|
8
16
|
* SSE comments (`: keepalive\n\n`) are ignored by standard SSE parsers,
|
|
9
17
|
* so no client changes are needed.
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle: a function-local `Scope` owns both the keepalive and body-pump
|
|
20
|
+
* fibers via `Effect.forkScoped`. Stream `cancel` closes the scope, which
|
|
21
|
+
* auto-interrupts both fibers and runs their `ensuring` finalizers. No
|
|
22
|
+
* manual `Fiber.interrupt` bookkeeping.
|
|
10
23
|
*/
|
|
11
24
|
export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAULT_KEEPALIVE_INTERVAL_MS): Response {
|
|
12
25
|
const body = response.body
|
|
@@ -14,24 +27,10 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
|
|
|
14
27
|
|
|
15
28
|
let closed = false
|
|
16
29
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
|
|
17
|
-
|
|
18
|
-
let bodyPumpFiber: ReturnType<typeof Effect.runFork> | null = null
|
|
19
|
-
|
|
20
|
-
const interruptFiber = (fiber: ReturnType<typeof Effect.runFork> | null) => {
|
|
21
|
-
if (!fiber) return
|
|
22
|
-
void Effect.runFork(Fiber.interrupt(fiber))
|
|
23
|
-
}
|
|
30
|
+
const scope = Scope.makeUnsafe()
|
|
24
31
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
keepaliveFiber = null
|
|
28
|
-
interruptFiber(fiber)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const stopBodyPump = () => {
|
|
32
|
-
const fiber = bodyPumpFiber
|
|
33
|
-
bodyPumpFiber = null
|
|
34
|
-
interruptFiber(fiber)
|
|
32
|
+
const closeScope = () => {
|
|
33
|
+
void Effect.runPromise(Scope.close(scope, Exit.void))
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
const releaseReader = (bodyReader: ReadableStreamDefaultReader<Uint8Array>) => {
|
|
@@ -90,7 +89,10 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
|
|
|
90
89
|
for (;;) {
|
|
91
90
|
if (closed) return
|
|
92
91
|
|
|
93
|
-
const { done, value } = yield* Effect.tryPromise(
|
|
92
|
+
const { done, value } = yield* Effect.tryPromise({
|
|
93
|
+
try: () => bodyReader.read(),
|
|
94
|
+
catch: (cause) => new SseKeepaliveError({ phase: 'read', cause }),
|
|
95
|
+
})
|
|
94
96
|
if (done) {
|
|
95
97
|
yield* Effect.sync(() => closeStream(controller))
|
|
96
98
|
return
|
|
@@ -105,25 +107,26 @@ export function wrapResponseWithKeepalive(response: Response, intervalMs = DEFAU
|
|
|
105
107
|
Effect.ensuring(
|
|
106
108
|
Effect.sync(() => {
|
|
107
109
|
closed = true
|
|
108
|
-
stopKeepalive()
|
|
109
|
-
bodyPumpFiber = null
|
|
110
110
|
reader = null
|
|
111
111
|
releaseReader(bodyReader)
|
|
112
|
+
closeScope()
|
|
112
113
|
}),
|
|
113
114
|
),
|
|
114
115
|
)
|
|
115
116
|
|
|
117
|
+
const startFiber = <A, E>(effect: Effect.Effect<A, E>): Fiber.Fiber<Fiber.Fiber<A, E>, never> =>
|
|
118
|
+
Effect.runFork(Scope.provide(Effect.forkScoped(effect), scope))
|
|
119
|
+
|
|
116
120
|
const transformed = new ReadableStream<Uint8Array>({
|
|
117
121
|
start(controller) {
|
|
118
122
|
const bodyReader = body.getReader()
|
|
119
123
|
reader = bodyReader
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
startFiber(keepaliveEffect(controller))
|
|
125
|
+
startFiber(pumpBodyEffect(bodyReader, controller))
|
|
122
126
|
},
|
|
123
127
|
cancel(reason) {
|
|
124
128
|
closed = true
|
|
125
|
-
|
|
126
|
-
stopBodyPump()
|
|
129
|
+
closeScope()
|
|
127
130
|
|
|
128
131
|
const bodyReader = reader
|
|
129
132
|
reader = null
|
package/src/workers/bootstrap.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import { ConfigProvider, Option, Schema, Effect, Layer, ManagedRuntime, Redacted } from 'effect'
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { ConfigProvider, Deferred, Option, Schema, Effect, Layer, ManagedRuntime, Redacted } from 'effect'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AiGatewayModelsTag,
|
|
5
|
+
AiGatewayTag,
|
|
6
|
+
RuntimeBridgeTag,
|
|
7
|
+
createAiGatewayModels,
|
|
8
|
+
makeAiGatewayService,
|
|
9
|
+
} from '../ai-gateway/ai-gateway'
|
|
10
|
+
import type { AiGatewayModels, RuntimeBridge } from '../ai-gateway/ai-gateway'
|
|
4
11
|
import { EmbeddingCacheLive } from '../ai/embedding-cache'
|
|
5
12
|
import { serverLogger } from '../config/logger'
|
|
6
13
|
import { connectWithStartupRetry, waitForDatabaseBootstrap } from '../db/startup'
|
|
7
14
|
import { buildWorkerInfrastructureLayer } from '../effect'
|
|
15
|
+
import { ERROR_TAGS } from '../effect/errors'
|
|
8
16
|
import { DatabaseServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
|
|
9
17
|
import { lotaRuntimeEnvConfig, parseLotaRuntimeConfig, parseWorkerBootstrapEnv } from '../runtime/runtime-config'
|
|
10
18
|
import { FirecrawlLive } from '../tools/firecrawl-client'
|
|
11
19
|
import { getErrorMessage } from '../utils/errors'
|
|
12
20
|
|
|
13
21
|
class SandboxedWorkerBootstrapError extends Schema.TaggedErrorClass<SandboxedWorkerBootstrapError>()(
|
|
14
|
-
|
|
22
|
+
ERROR_TAGS.SandboxedWorkerBootstrapError,
|
|
15
23
|
{
|
|
16
24
|
stage: Schema.Literals(['setup', 'initialize', 'connect-db', 'connect-plugin-db', 'bootstrap-wait']),
|
|
17
25
|
message: Schema.String,
|
|
@@ -86,12 +94,35 @@ function ensureSandboxedWorkerRuntimeConfigured(): Promise<WorkerManagedRuntime>
|
|
|
86
94
|
const runtimeConfig = yield* buildSandboxedWorkerRuntimeConfigEffect()
|
|
87
95
|
|
|
88
96
|
const infrastructureLayer = buildWorkerInfrastructureLayer(runtimeConfig)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
97
|
+
const aiGateway = yield* makeAiGatewayService(runtimeConfig).pipe(
|
|
98
|
+
Effect.mapError((error) => toSandboxedWorkerBootstrapError('setup', error)),
|
|
99
|
+
)
|
|
100
|
+
const aiGatewayModelsDeferred = yield* Deferred.make<AiGatewayModels>()
|
|
101
|
+
const runtimeBridgeDeferred = yield* Deferred.make<RuntimeBridge>()
|
|
102
|
+
const bridgeLayer = Layer.mergeAll(
|
|
103
|
+
Layer.succeed(AiGatewayTag, aiGateway),
|
|
104
|
+
Layer.effect(AiGatewayModelsTag, Deferred.await(aiGatewayModelsDeferred)),
|
|
105
|
+
Layer.effect(RuntimeBridgeTag, Deferred.await(runtimeBridgeDeferred)),
|
|
106
|
+
)
|
|
107
|
+
const layerWithBridge = Layer.mergeAll(infrastructureLayer, bridgeLayer)
|
|
108
|
+
const fullLayer = Layer.mergeAll(
|
|
109
|
+
layerWithBridge,
|
|
110
|
+
Layer.provide(Layer.mergeAll(EmbeddingCacheLive, FirecrawlLive), layerWithBridge),
|
|
111
|
+
)
|
|
93
112
|
|
|
94
113
|
const managedRuntime = ManagedRuntime.make(fullLayer)
|
|
114
|
+
const runtimeBridge: RuntimeBridge = {
|
|
115
|
+
runPromise: (effect, options) => managedRuntime.runPromise(effect, options),
|
|
116
|
+
runFork: (effect) => managedRuntime.runFork(effect),
|
|
117
|
+
}
|
|
118
|
+
const aiGatewayModels = createAiGatewayModels({
|
|
119
|
+
gateway: aiGateway,
|
|
120
|
+
runtimeConfig,
|
|
121
|
+
runPromise: runtimeBridge.runPromise,
|
|
122
|
+
runFork: runtimeBridge.runFork,
|
|
123
|
+
})
|
|
124
|
+
yield* Deferred.succeed(runtimeBridgeDeferred, runtimeBridge)
|
|
125
|
+
yield* Deferred.succeed(aiGatewayModelsDeferred, aiGatewayModels)
|
|
95
126
|
|
|
96
127
|
return managedRuntime
|
|
97
128
|
}),
|
|
@@ -111,7 +142,10 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
|
|
|
111
142
|
// Assign before the async kicks off so concurrent callers observe the in-flight promise.
|
|
112
143
|
sandboxedWorkerInitPromise = Effect.runPromise(
|
|
113
144
|
Effect.gen(function* () {
|
|
114
|
-
const env =
|
|
145
|
+
const env = yield* Effect.try({
|
|
146
|
+
try: () => parseWorkerBootstrapEnv(Bun.env),
|
|
147
|
+
catch: (error) => toSandboxedWorkerBootstrapError('setup', error),
|
|
148
|
+
})
|
|
115
149
|
const runtime = yield* Effect.tryPromise({
|
|
116
150
|
try: () => ensureSandboxedWorkerRuntimeConfigured(),
|
|
117
151
|
catch: (error) => toSandboxedWorkerBootstrapError('setup', error),
|
|
@@ -124,7 +158,7 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
|
|
|
124
158
|
yield* Effect.tryPromise({
|
|
125
159
|
try: () =>
|
|
126
160
|
connectWithStartupRetry({
|
|
127
|
-
connect: () => db.connect(),
|
|
161
|
+
connect: () => runtime.runPromise(db.connect()),
|
|
128
162
|
label: 'sandboxed worker AI database runtime',
|
|
129
163
|
logger: serverLogger,
|
|
130
164
|
}),
|
|
@@ -156,7 +190,7 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
|
|
|
156
190
|
expectedFingerprint: env.DB_SCHEMA_FINGERPRINT,
|
|
157
191
|
label: 'sandboxed worker runtime',
|
|
158
192
|
logger: serverLogger,
|
|
159
|
-
connect: () => db.connect(),
|
|
193
|
+
connect: () => runtime.runPromise(db.connect()),
|
|
160
194
|
}),
|
|
161
195
|
catch: (error) => toSandboxedWorkerBootstrapError('bootstrap-wait', error),
|
|
162
196
|
})
|
|
@@ -170,3 +204,33 @@ export function initializeSandboxedWorkerRuntime(): Promise<WorkerManagedRuntime
|
|
|
170
204
|
|
|
171
205
|
return sandboxedWorkerInitPromise
|
|
172
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Dispose the sandboxed worker runtime and clear both module-level caches so
|
|
210
|
+
* a subsequent initialize call rebuilds a fresh runtime. Registered as a
|
|
211
|
+
* SIGTERM/SIGINT handler when the worker process runs as the main entrypoint.
|
|
212
|
+
*/
|
|
213
|
+
// @effect-diagnostics-next-line asyncFunction:off -- host-boundary dispose handler returns Promise by design.
|
|
214
|
+
export async function disposeSandboxedWorkerRuntime(): Promise<void> {
|
|
215
|
+
const setupPromise = sandboxedWorkerSetupPromise
|
|
216
|
+
sandboxedWorkerSetupPromise = null
|
|
217
|
+
sandboxedWorkerInitPromise = null
|
|
218
|
+
if (!setupPromise) return
|
|
219
|
+
try {
|
|
220
|
+
const runtime = await setupPromise
|
|
221
|
+
await runtime.dispose()
|
|
222
|
+
} catch (error) {
|
|
223
|
+
serverLogger.warn`Failed to dispose sandboxed worker runtime: ${error}`
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let sandboxedWorkerShutdownHandlersRegistered = false
|
|
228
|
+
export function registerSandboxedWorkerShutdownHandlers(): void {
|
|
229
|
+
if (sandboxedWorkerShutdownHandlersRegistered) return
|
|
230
|
+
sandboxedWorkerShutdownHandlersRegistered = true
|
|
231
|
+
const handle = () => {
|
|
232
|
+
void disposeSandboxedWorkerRuntime().catch(() => undefined)
|
|
233
|
+
}
|
|
234
|
+
process.once('SIGTERM', handle)
|
|
235
|
+
process.once('SIGINT', handle)
|
|
236
|
+
}
|