mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks (#3399)
* fix(monitoring): set MemoryTelemetry logger to INFO level for production visibility Production defaults to ERROR-only logging. Without this override, memory snapshots would be silently suppressed. * fix(memory): fix O(n²) string concatenation and unconsumed fetch response leaks * fix(tests): add text() mock to workflow-handler test fetch responses * fix(memory): remove unused O(n²) join in onStreamChunk callback
This commit is contained in:
@@ -711,7 +711,7 @@ async function handleMessageStream(
|
||||
if (response.body && isStreamingResponse) {
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let accumulatedContent = ''
|
||||
const contentChunks: string[] = []
|
||||
let finalContent: string | undefined
|
||||
|
||||
while (true) {
|
||||
@@ -722,7 +722,7 @@ async function handleMessageStream(
|
||||
const parsed = parseWorkflowSSEChunk(rawChunk)
|
||||
|
||||
if (parsed.content) {
|
||||
accumulatedContent += parsed.content
|
||||
contentChunks.push(parsed.content)
|
||||
sendEvent('message', {
|
||||
kind: 'message',
|
||||
taskId,
|
||||
@@ -738,6 +738,7 @@ async function handleMessageStream(
|
||||
}
|
||||
}
|
||||
|
||||
const accumulatedContent = contentChunks.join('')
|
||||
const messageContent =
|
||||
(finalContent !== undefined && finalContent.length > 0
|
||||
? finalContent
|
||||
|
||||
@@ -405,13 +405,17 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
controller.close()
|
||||
try {
|
||||
controller.close()
|
||||
} catch {
|
||||
// controller may already be closed by cancel()
|
||||
}
|
||||
}
|
||||
},
|
||||
async cancel() {
|
||||
clientDisconnected = true
|
||||
if (eventWriter) {
|
||||
await eventWriter.flush()
|
||||
await eventWriter.close().catch(() => {})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -342,6 +342,7 @@ async function generateWithRunway(
|
||||
})
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
await statusResponse.text().catch(() => {})
|
||||
throw new Error(`Runway status check failed: ${statusResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -352,6 +353,7 @@ async function generateWithRunway(
|
||||
|
||||
const videoResponse = await fetch(statusData.output[0])
|
||||
if (!videoResponse.ok) {
|
||||
await videoResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video: ${videoResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -448,6 +450,7 @@ async function generateWithVeo(
|
||||
)
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
await statusResponse.text().catch(() => {})
|
||||
throw new Error(`Veo status check failed: ${statusResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -472,6 +475,7 @@ async function generateWithVeo(
|
||||
})
|
||||
|
||||
if (!videoResponse.ok) {
|
||||
await videoResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video: ${videoResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -561,6 +565,7 @@ async function generateWithLuma(
|
||||
)
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
await statusResponse.text().catch(() => {})
|
||||
throw new Error(`Luma status check failed: ${statusResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -576,6 +581,7 @@ async function generateWithLuma(
|
||||
|
||||
const videoResponse = await fetch(videoUrl)
|
||||
if (!videoResponse.ok) {
|
||||
await videoResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video: ${videoResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -679,6 +685,7 @@ async function generateWithMiniMax(
|
||||
)
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
await statusResponse.text().catch(() => {})
|
||||
throw new Error(`MiniMax status check failed: ${statusResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -712,6 +719,7 @@ async function generateWithMiniMax(
|
||||
)
|
||||
|
||||
if (!fileResponse.ok) {
|
||||
await fileResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video: ${fileResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -725,6 +733,7 @@ async function generateWithMiniMax(
|
||||
// Download the actual video file
|
||||
const videoResponse = await fetch(videoUrl)
|
||||
if (!videoResponse.ok) {
|
||||
await videoResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video from URL: ${videoResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -881,6 +890,7 @@ async function generateWithFalAI(
|
||||
)
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
await statusResponse.text().catch(() => {})
|
||||
throw new Error(`Fal.ai status check failed: ${statusResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -899,6 +909,7 @@ async function generateWithFalAI(
|
||||
)
|
||||
|
||||
if (!resultResponse.ok) {
|
||||
await resultResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to fetch result: ${resultResponse.status}`)
|
||||
}
|
||||
|
||||
@@ -911,6 +922,7 @@ async function generateWithFalAI(
|
||||
|
||||
const videoResponse = await fetch(videoUrl)
|
||||
if (!videoResponse.ok) {
|
||||
await videoResponse.text().catch(() => {})
|
||||
throw new Error(`Failed to download video: ${videoResponse.status}`)
|
||||
}
|
||||
|
||||
|
||||
+20
-12
@@ -745,7 +745,7 @@ export function useWorkflowExecution() {
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const { encodeSSE } = await import('@/lib/core/utils/sse')
|
||||
const streamedContent = new Map<string, string>()
|
||||
const streamedChunks = new Map<string, string[]>()
|
||||
const streamReadingPromises: Promise<void>[] = []
|
||||
|
||||
const safeEnqueue = (data: Uint8Array) => {
|
||||
@@ -845,8 +845,8 @@ export function useWorkflowExecution() {
|
||||
const reader = streamingExecution.stream.getReader()
|
||||
const blockId = (streamingExecution.execution as any)?.blockId
|
||||
|
||||
if (blockId && !streamedContent.has(blockId)) {
|
||||
streamedContent.set(blockId, '')
|
||||
if (blockId && !streamedChunks.has(blockId)) {
|
||||
streamedChunks.set(blockId, [])
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -860,13 +860,13 @@ export function useWorkflowExecution() {
|
||||
}
|
||||
const chunk = new TextDecoder().decode(value)
|
||||
if (blockId) {
|
||||
streamedContent.set(blockId, (streamedContent.get(blockId) || '') + chunk)
|
||||
streamedChunks.get(blockId)!.push(chunk)
|
||||
}
|
||||
|
||||
let chunkToSend = chunk
|
||||
if (blockId && !processedFirstChunk.has(blockId)) {
|
||||
processedFirstChunk.add(blockId)
|
||||
if (streamedContent.size > 1) {
|
||||
if (streamedChunks.size > 1) {
|
||||
chunkToSend = `\n\n${chunk}`
|
||||
}
|
||||
}
|
||||
@@ -884,7 +884,7 @@ export function useWorkflowExecution() {
|
||||
// Handle non-streaming blocks (like Function blocks)
|
||||
const onBlockComplete = async (blockId: string, output: any) => {
|
||||
// Skip if this block already had streaming content (avoid duplicates)
|
||||
if (streamedContent.has(blockId)) {
|
||||
if (streamedChunks.has(blockId)) {
|
||||
logger.debug('[handleRunWorkflow] Skipping onBlockComplete for streaming block', {
|
||||
blockId,
|
||||
})
|
||||
@@ -921,13 +921,13 @@ export function useWorkflowExecution() {
|
||||
: JSON.stringify(outputValue, null, 2)
|
||||
|
||||
// Add separator if this isn't the first output
|
||||
const separator = streamedContent.size > 0 ? '\n\n' : ''
|
||||
const separator = streamedChunks.size > 0 ? '\n\n' : ''
|
||||
|
||||
// Send the non-streaming block output as a chunk
|
||||
safeEnqueue(encodeSSE({ blockId, chunk: separator + formattedOutput }))
|
||||
|
||||
// Track that we've sent output for this block
|
||||
streamedContent.set(blockId, formattedOutput)
|
||||
streamedChunks.set(blockId, [formattedOutput])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -969,6 +969,12 @@ export function useWorkflowExecution() {
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve chunks to final strings for consumption
|
||||
const streamedContent = new Map<string, string>()
|
||||
for (const [id, chunks] of streamedChunks) {
|
||||
streamedContent.set(id, chunks.join(''))
|
||||
}
|
||||
|
||||
// Update streamed content and apply tokenization
|
||||
if (result.logs) {
|
||||
result.logs.forEach((log: BlockLog) => {
|
||||
@@ -1316,7 +1322,7 @@ export function useWorkflowExecution() {
|
||||
|
||||
const activeBlocksSet = new Set<string>()
|
||||
const activeBlockRefCounts = new Map<string, number>()
|
||||
const streamedContent = new Map<string, string>()
|
||||
const streamedChunks = new Map<string, string[]>()
|
||||
const accumulatedBlockLogs: BlockLog[] = []
|
||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||
const executedBlockIds = new Set<string>()
|
||||
@@ -1374,8 +1380,10 @@ export function useWorkflowExecution() {
|
||||
onBlockChildWorkflowStarted: blockHandlers.onBlockChildWorkflowStarted,
|
||||
|
||||
onStreamChunk: (data) => {
|
||||
const existing = streamedContent.get(data.blockId) || ''
|
||||
streamedContent.set(data.blockId, existing + data.chunk)
|
||||
if (!streamedChunks.has(data.blockId)) {
|
||||
streamedChunks.set(data.blockId, [])
|
||||
}
|
||||
streamedChunks.get(data.blockId)!.push(data.chunk)
|
||||
|
||||
// Call onStream callback if provided (create a fake StreamingExecution)
|
||||
if (onStream && isExecutingFromChat) {
|
||||
@@ -1390,7 +1398,7 @@ export function useWorkflowExecution() {
|
||||
stream,
|
||||
execution: {
|
||||
success: true,
|
||||
output: { content: existing + data.chunk },
|
||||
output: { content: '' },
|
||||
blockId: data.blockId,
|
||||
} as any,
|
||||
}
|
||||
|
||||
@@ -357,6 +357,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error(`Failed to fetch custom tools: ${response.status}`)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -116,21 +116,22 @@ export class Memory {
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs
|
||||
): ReadableStream<Uint8Array> {
|
||||
let accumulatedContent = ''
|
||||
const chunks: string[] = []
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
const transformStream = new TransformStream<Uint8Array, Uint8Array>({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(chunk)
|
||||
const decoded = decoder.decode(chunk, { stream: true })
|
||||
accumulatedContent += decoded
|
||||
chunks.push(decoded)
|
||||
},
|
||||
|
||||
flush: () => {
|
||||
if (accumulatedContent.trim()) {
|
||||
const content = chunks.join('')
|
||||
if (content.trim()) {
|
||||
this.appendToMemory(ctx, inputs, {
|
||||
role: 'assistant',
|
||||
content: accumulatedContent,
|
||||
content,
|
||||
}).catch((error) => logger.error('Failed to persist streaming response:', error))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,6 +142,7 @@ describe('WorkflowBlockHandler', () => {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: () => Promise.resolve(''),
|
||||
})
|
||||
|
||||
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
|
||||
@@ -168,6 +169,7 @@ describe('WorkflowBlockHandler', () => {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
text: () => Promise.resolve(''),
|
||||
})
|
||||
|
||||
const result = await (handler as any).loadChildWorkflow(workflowId)
|
||||
|
||||
@@ -375,6 +375,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
const response = await fetch(url.toString(), { headers })
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
if (response.status === HTTP.STATUS.NOT_FOUND) {
|
||||
logger.warn(`Child workflow ${workflowId} not found`)
|
||||
return null
|
||||
|
||||
@@ -77,6 +77,7 @@ export async function deliverPushNotification(taskId: string, state: TaskState):
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Push notification delivery failed', {
|
||||
taskId,
|
||||
url: config.url,
|
||||
|
||||
@@ -773,6 +773,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!profileResponse.ok) {
|
||||
await profileResponse.text().catch(() => {})
|
||||
logger.error('Failed to fetch GitHub profile', {
|
||||
status: profileResponse.status,
|
||||
statusText: profileResponse.statusText,
|
||||
@@ -850,6 +851,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -889,6 +891,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -929,6 +932,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -969,6 +973,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1009,6 +1014,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1049,6 +1055,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1090,6 +1097,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1129,6 +1137,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1170,6 +1179,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1211,6 +1221,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1251,6 +1262,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1291,6 +1303,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Google user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1352,6 +1365,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1391,6 +1405,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1485,6 +1500,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1533,6 +1549,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1572,6 +1589,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1619,6 +1637,7 @@ export const auth = betterAuth({
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
})
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Microsoft user info', { status: response.status })
|
||||
throw new Error(`Failed to fetch Microsoft user info: ${response.statusText}`)
|
||||
}
|
||||
@@ -1701,6 +1720,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Pipedrive user info', {
|
||||
status: response.status,
|
||||
})
|
||||
@@ -1848,6 +1868,7 @@ export const auth = betterAuth({
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Salesforce user info', {
|
||||
status: response.status,
|
||||
})
|
||||
@@ -1915,6 +1936,7 @@ export const auth = betterAuth({
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching X user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2009,6 +2031,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Confluence user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2120,6 +2143,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Jira user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2177,6 +2201,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Airtable user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2226,6 +2251,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Notion user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2293,6 +2319,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Reddit user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2540,6 +2567,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Asana user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2606,6 +2634,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Slack auth.test failed', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2665,6 +2694,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Error fetching Webflow user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2716,6 +2746,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch LinkedIn user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2778,6 +2809,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Zoom user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2845,6 +2877,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Spotify user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2893,6 +2926,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch WordPress.com user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
@@ -2942,6 +2976,7 @@ export const auth = betterAuth({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Cal.com user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
|
||||
@@ -43,6 +43,7 @@ export function useSubscriptionUpgrade() {
|
||||
try {
|
||||
const orgsResponse = await fetch('/api/organizations')
|
||||
if (!orgsResponse.ok) {
|
||||
await orgsResponse.text().catch(() => {})
|
||||
throw new Error('Failed to check organization status')
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export async function saveMessageCheckpoint(
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
throw new Error(`Failed to create checkpoint: ${response.statusText}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function fetchPersonalEnvironment(): Promise<Record<string, Environ
|
||||
const response = await fetch(API_ENDPOINTS.ENVIRONMENT)
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
throw new Error(`Failed to load environment variables: ${response.statusText}`)
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ export async function fetchWorkspaceEnvironment(
|
||||
const response = await fetch(API_ENDPOINTS.WORKSPACE_ENVIRONMENT(workspaceId))
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
throw new Error(`Failed to load workspace environment: ${response.statusText}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +278,7 @@ async function fetchNewRssItems(
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
throw new Error(`Failed to fetch RSS feed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,19 @@ export interface StreamingResponseOptions {
|
||||
}
|
||||
|
||||
interface StreamingState {
|
||||
streamedContent: Map<string, string>
|
||||
streamedChunks: Map<string, string[]>
|
||||
processedOutputs: Set<string>
|
||||
streamCompletionTimes: Map<string, number>
|
||||
}
|
||||
|
||||
function resolveStreamedContent(state: StreamingState): Map<string, string> {
|
||||
const result = new Map<string, string>()
|
||||
for (const [blockId, chunks] of state.streamedChunks) {
|
||||
result.set(blockId, chunks.join(''))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function extractOutputValue(output: unknown, path: string): unknown {
|
||||
return traverseObjectPath(output, path)
|
||||
}
|
||||
@@ -125,17 +133,21 @@ async function buildMinimalResult(
|
||||
return minimalResult
|
||||
}
|
||||
|
||||
function updateLogsWithStreamedContent(logs: BlockLog[], state: StreamingState): BlockLog[] {
|
||||
function updateLogsWithStreamedContent(
|
||||
logs: BlockLog[],
|
||||
streamedContent: Map<string, string>,
|
||||
streamCompletionTimes: Map<string, number>
|
||||
): BlockLog[] {
|
||||
return logs.map((log: BlockLog) => {
|
||||
if (!state.streamedContent.has(log.blockId)) {
|
||||
if (!streamedContent.has(log.blockId)) {
|
||||
return log
|
||||
}
|
||||
|
||||
const content = state.streamedContent.get(log.blockId)
|
||||
const content = streamedContent.get(log.blockId)
|
||||
const updatedLog = { ...log }
|
||||
|
||||
if (state.streamCompletionTimes.has(log.blockId)) {
|
||||
const completionTime = state.streamCompletionTimes.get(log.blockId)!
|
||||
if (streamCompletionTimes.has(log.blockId)) {
|
||||
const completionTime = streamCompletionTimes.get(log.blockId)!
|
||||
const startTime = new Date(log.startedAt).getTime()
|
||||
updatedLog.endedAt = new Date(completionTime).toISOString()
|
||||
updatedLog.durationMs = completionTime - startTime
|
||||
@@ -176,7 +188,7 @@ export async function createStreamingResponse(
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
const state: StreamingState = {
|
||||
streamedContent: new Map(),
|
||||
streamedChunks: new Map(),
|
||||
processedOutputs: new Set(),
|
||||
streamCompletionTimes: new Map(),
|
||||
}
|
||||
@@ -210,10 +222,10 @@ export async function createStreamingResponse(
|
||||
}
|
||||
|
||||
const textChunk = decoder.decode(value, { stream: true })
|
||||
state.streamedContent.set(
|
||||
blockId,
|
||||
(state.streamedContent.get(blockId) || '') + textChunk
|
||||
)
|
||||
if (!state.streamedChunks.has(blockId)) {
|
||||
state.streamedChunks.set(blockId, [])
|
||||
}
|
||||
state.streamedChunks.get(blockId)!.push(textChunk)
|
||||
|
||||
if (isFirstChunk) {
|
||||
sendChunk(blockId, textChunk)
|
||||
@@ -242,7 +254,7 @@ export async function createStreamingResponse(
|
||||
return
|
||||
}
|
||||
|
||||
if (state.streamedContent.has(blockId)) {
|
||||
if (state.streamedChunks.has(blockId)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -292,9 +304,16 @@ export async function createStreamingResponse(
|
||||
executionId
|
||||
)
|
||||
|
||||
if (result.logs && state.streamedContent.size > 0) {
|
||||
result.logs = updateLogsWithStreamedContent(result.logs, state)
|
||||
processStreamingBlockLogs(result.logs, state.streamedContent)
|
||||
const streamedContent =
|
||||
state.streamedChunks.size > 0 ? resolveStreamedContent(state) : new Map<string, string>()
|
||||
|
||||
if (result.logs && streamedContent.size > 0) {
|
||||
result.logs = updateLogsWithStreamedContent(
|
||||
result.logs,
|
||||
streamedContent,
|
||||
state.streamCompletionTimes
|
||||
)
|
||||
processStreamingBlockLogs(result.logs, streamedContent)
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -316,7 +335,7 @@ export async function createStreamingResponse(
|
||||
const minimalResult = await buildMinimalResult(
|
||||
result,
|
||||
streamConfig.selectedOutputs,
|
||||
state.streamedContent,
|
||||
streamedContent,
|
||||
requestId,
|
||||
streamConfig.includeFileBase64 ?? true,
|
||||
streamConfig.base64MaxBytes
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ollamaProvider: ProviderConfig = {
|
||||
try {
|
||||
const response = await fetch(`${OLLAMA_HOST}/api/tags`)
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
useProvidersStore.getState().setProviderModels('ollama', [])
|
||||
logger.warn('Ollama service is not available. The provider will be disabled.')
|
||||
return
|
||||
|
||||
@@ -29,6 +29,7 @@ async function fetchModelCapabilities(): Promise<Map<string, ModelCapabilities>>
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.warn('Failed to fetch OpenRouter model capabilities', {
|
||||
status: response.status,
|
||||
})
|
||||
|
||||
@@ -73,6 +73,7 @@ async function fetchWorkflowMetadata(
|
||||
|
||||
const response = await fetch(url.toString(), { headers })
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.warn(`Failed to fetch workflow metadata for ${workflowId}`)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export const vllmProvider: ProviderConfig = {
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/models`, { headers })
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
useProvidersStore.getState().setProviderModels('vllm', [])
|
||||
logger.warn('vLLM service is not available. The provider will be disabled.')
|
||||
return
|
||||
|
||||
@@ -401,6 +401,7 @@ async function fetchCustomToolFromAPI(
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error(`Failed to fetch custom tools: ${response.statusText}`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user