Compare commits

...

11 Commits

Author SHA1 Message Date
Waleed Latif
d21dc65782 fix(chat): use explicit trigger type check instead of heuristic for chat guard 2026-03-04 18:48:19 -08:00
Waleed Latif
72eb8924a8 upgrade turborepo 2026-03-04 17:37:47 -08:00
Waleed Latif
f5651ec3ba fix(servicenow): remove invalid string comparison on numeric offset param 2026-03-04 17:36:59 -08:00
Waleed Latif
3e3208e42f fix(chat): remove useEffect anti-pattern that killed chat stream on state change
The effect reacted to isExecuting becoming false to clean up streams,
but this is an anti-pattern per React guidelines — using state changes
as a proxy for events. All cleanup cases are already handled by proper
event paths: stream done (processStreamingResponse), user cancel
(handleStopStreaming), component unmount (cleanup effect), and
abort/error (catch block).
2026-03-04 17:36:17 -08:00
Waleed Latif
726fb4cfc8 fix(chat): prevent premature isExecuting=false from killing chat stream
The onExecutionCompleted/Error/Cancelled callbacks were setting
isExecuting=false as soon as the server-side SSE stream completed.
For chat executions, this triggered a useEffect in chat.tsx that
cancelled the client-side stream reader before it finished consuming
buffered data — causing empty or partial chat responses.

Skip the isExecuting=false in these callbacks for chat executions
since the chat's own finally block handles cleanup after the stream
is fully consumed.
2026-03-04 17:30:36 -08:00
Waleed Latif
d7df044baa fix(memory): use text() drain for SecureFetchResponse which lacks body property 2026-03-04 17:20:53 -08:00
Waleed Latif
ca16db0e3d fix(executor): flush TextDecoder after streaming loop for multi-byte chars 2026-03-04 17:16:38 -08:00
Waleed Latif
4dba2a9e77 fix(memory): use response.body.cancel() instead of response.text() for drains 2026-03-04 17:16:38 -08:00
Waleed Latif
2a9df5a821 fix(memory): update Bun.gc comment to match non-blocking call 2026-03-04 17:16:38 -08:00
Waleed Latif
52bb7e0294 fix(memory): await reader.cancel() and use non-blocking Bun.gc 2026-03-04 17:16:38 -08:00
Waleed Latif
7efb8acac1 fix(memory): add Bun.gc, stream cancellation, and unconsumed fetch drains 2026-03-04 17:16:38 -08:00
15 changed files with 46 additions and 29 deletions

View File

@@ -150,6 +150,7 @@ export async function POST(request: NextRequest) {
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
throw new Error(`Failed to download audio from URL: ${response.statusText}`)
}

View File

@@ -135,6 +135,7 @@ async function fetchDocumentBytes(url: string): Promise<{ bytes: string; content
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
throw new Error(`Failed to fetch document: ${response.statusText}`)
}

View File

@@ -65,6 +65,7 @@ export async function POST(request: NextRequest) {
})
if (!response.ok) {
await response.body?.cancel().catch(() => {})
logger.error(`Failed to generate TTS: ${response.status} ${response.statusText}`)
return NextResponse.json(
{ error: `Failed to generate TTS: ${response.status} ${response.statusText}` },

View File

@@ -184,6 +184,7 @@ export async function POST(request: NextRequest) {
method: 'GET',
})
if (!response.ok) {
await response.text().catch(() => {})
return NextResponse.json(
{ success: false, error: 'Failed to fetch image for Gemini' },
{ status: 400 }

View File

@@ -964,7 +964,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
logger.error(`[${requestId}] Error streaming block content:`, error)
} finally {
try {
reader.releaseLock()
await reader.cancel().catch(() => {})
} catch {}
}
}

View File

@@ -501,17 +501,6 @@ export function Chat() {
}
}, [])
useEffect(() => {
if (!isExecuting && isStreaming) {
const lastMessage = workflowMessages[workflowMessages.length - 1]
if (lastMessage?.isStreaming) {
streamReaderRef.current?.cancel()
streamReaderRef.current = null
finalizeMessageStream(lastMessage.id)
}
}
}, [isExecuting, isStreaming, workflowMessages, finalizeMessageStream])
const handleStopStreaming = useCallback(() => {
streamReaderRef.current?.cancel()
streamReaderRef.current = null

View File

@@ -1495,8 +1495,13 @@ export function useWorkflowExecution() {
: null
if (activeWorkflowId && !workflowExecState?.isDebugging) {
setExecutionResult(executionResult)
setIsExecuting(activeWorkflowId, false)
setActiveBlocks(activeWorkflowId, new Set())
// For chat executions, don't set isExecuting=false here — the chat's
// client-side stream wrapper still has buffered data to deliver.
// The chat's finally block handles cleanup after the stream is fully consumed.
if (overrideTriggerType !== 'chat') {
setIsExecuting(activeWorkflowId, false)
setActiveBlocks(activeWorkflowId, new Set())
}
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
}, 1000)
@@ -1536,7 +1541,7 @@ export function useWorkflowExecution() {
isPreExecutionError,
})
if (activeWorkflowId) {
if (activeWorkflowId && overrideTriggerType !== 'chat') {
setIsExecuting(activeWorkflowId, false)
setIsDebugging(activeWorkflowId, false)
setActiveBlocks(activeWorkflowId, new Set())
@@ -1562,7 +1567,7 @@ export function useWorkflowExecution() {
durationMs: data?.duration,
})
if (activeWorkflowId) {
if (activeWorkflowId && overrideTriggerType !== 'chat') {
setIsExecuting(activeWorkflowId, false)
setIsDebugging(activeWorkflowId, false)
setActiveBlocks(activeWorkflowId, new Set())

View File

@@ -618,6 +618,8 @@ export class BlockExecutor {
await ctx.onStream?.(clientStreamingExec)
} catch (error) {
logger.error('Error in onStream callback', { blockId, error })
// Cancel the client stream to release the tee'd buffer
await processedClientStream.cancel().catch(() => {})
}
})()
@@ -646,6 +648,7 @@ export class BlockExecutor {
})
} catch (error) {
logger.error('Error in onStream callback', { blockId, error })
await processedStream.cancel().catch(() => {})
}
}
@@ -657,22 +660,25 @@ export class BlockExecutor {
): Promise<void> {
const reader = stream.getReader()
const decoder = new TextDecoder()
let fullContent = ''
const chunks: string[] = []
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
fullContent += decoder.decode(value, { stream: true })
chunks.push(decoder.decode(value, { stream: true }))
}
const tail = decoder.decode()
if (tail) chunks.push(tail)
} catch (error) {
logger.error('Error reading executor stream for block', { blockId, error })
} finally {
try {
reader.releaseLock()
await reader.cancel().catch(() => {})
} catch {}
}
const fullContent = chunks.join('')
if (!fullContent) {
return
}

View File

@@ -23,6 +23,16 @@ export function startMemoryTelemetry(intervalMs = 60_000) {
started = true
const timer = setInterval(() => {
// Trigger opportunistic (non-blocking) garbage collection if running on Bun.
// This signals JSC GC + mimalloc page purge without blocking the event loop,
// helping reclaim RSS that mimalloc otherwise retains under sustained load.
const bunGlobal = (globalThis as Record<string, unknown>).Bun as
| { gc?: (force: boolean) => void }
| undefined
if (typeof bunGlobal?.gc === 'function') {
bunGlobal.gc(false)
}
const mem = process.memoryUsage()
const heap = v8.getHeapStatistics()

View File

@@ -759,6 +759,7 @@ async function markEmailAsRead(accessToken: string, messageId: string) {
})
if (!response.ok) {
await response.body?.cancel().catch(() => {})
throw new Error(
`Failed to mark email ${messageId} as read: ${response.status} ${response.statusText}`
)

View File

@@ -95,6 +95,7 @@ const nextConfig: NextConfig = {
optimizeCss: true,
turbopackSourceMaps: false,
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
},
...(isDev && {
allowedDevOrigins: [

View File

@@ -239,6 +239,7 @@ export async function downloadAttachments(
)
if (!attachmentResponse.ok) {
await attachmentResponse.body?.cancel().catch(() => {})
continue
}

View File

@@ -109,7 +109,7 @@ export const readRecordTool: ToolConfig<ServiceNowReadParams, ServiceNowReadResp
queryParams.append('sysparm_limit', params.limit.toString())
}
if (params.offset !== undefined && params.offset !== null && params.offset !== '') {
if (params.offset !== undefined && params.offset !== null) {
queryParams.append('sysparm_offset', params.offset.toString())
}

View File

@@ -13,7 +13,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.12",
"turbo": "2.8.13",
},
},
"apps/docs": {
@@ -3493,19 +3493,19 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.12", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.12", "turbo-darwin-arm64": "2.8.12", "turbo-linux-64": "2.8.12", "turbo-linux-arm64": "2.8.12", "turbo-windows-64": "2.8.12", "turbo-windows-arm64": "2.8.12" }, "bin": { "turbo": "bin/turbo" } }, "sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.12", "", { "os": "win32", "cpu": "x64" }, "sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],

View File

@@ -42,7 +42,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.12"
"turbo": "2.8.13"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [