diff --git a/apps/server/package.json b/apps/server/package.json index 6ee1931b..b836a30a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -11,7 +11,7 @@ "start": "cross-env NODE_ENV=development nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", - "start:prod": "cross-env NODE_ENV=production node dist/main", + "start:prod": "cross-env NODE_ENV=production node --heapsnapshot-near-heap-limit=2 dist/main", "collab:prod": "cross-env NODE_ENV=production node dist/collaboration/server/collab-main", "collab:dev": "cross-env NODE_ENV=development node dist/collaboration/server/collab-main", "email:dev": "email dev -p 5019 -d ./src/integrations/transactional/emails", diff --git a/apps/server/src/core/ai-chat/ai-chat.service.ts b/apps/server/src/core/ai-chat/ai-chat.service.ts index 1cce9cf3..16ba5824 100644 --- a/apps/server/src/core/ai-chat/ai-chat.service.ts +++ b/apps/server/src/core/ai-chat/ai-chat.service.ts @@ -535,6 +535,19 @@ export class AiChatService { }, }); + // Drain the stream independently of the client socket so the turn always + // runs to completion (or to its abort) and the terminal callbacks + // (onFinish/onError/onAbort) fire — releasing the per-turn object graph + // (history, the per-request toolset closures, captured steps, SDK buffers) + // and closing leased MCP clients. WITHOUT this, a client disconnect leaves + // the pipe's dead socket as the only reader; backpressure stalls the stream, + // the callbacks never run, and every dropped turn stays rooted in memory — + // the heap-OOM leak. consumeStream removes that backpressure (AI SDK v6 + // "Handling client disconnects"). NOT awaited (fire-and-forget); the stream + // errors are already logged by the streamText `onError` callback above, so + // swallow here to avoid an unhandledRejection. + void result.consumeStream({ onError: () => undefined }); + // Stream the UI-message protocol straight to the hijacked Node response. // Without onError the AI SDK masks the cause ('An error occurred.') and the // UI shows a generic failure. Surface the real provider message instead. diff --git a/apps/server/src/core/ai-chat/public-share-chat.service.ts b/apps/server/src/core/ai-chat/public-share-chat.service.ts index f2d8f0f8..8011814b 100644 --- a/apps/server/src/core/ai-chat/public-share-chat.service.ts +++ b/apps/server/src/core/ai-chat/public-share-chat.service.ts @@ -244,6 +244,15 @@ export class PublicShareChatService { }, }); + // Drain the stream independently of the client socket so the turn always + // runs to completion (or to its abort) even when the anonymous client + // disconnects — otherwise the dead socket is the only reader, backpressure + // stalls the stream, and the per-turn object graph stays rooted (heap-OOM + // leak). consumeStream removes that backpressure (AI SDK v6 "Handling + // client disconnects"). Fire-and-forget; stream errors are already logged + // by the streamText `onError` callback above. + void result.consumeStream({ onError: () => undefined }); + // Stream the UI-message protocol straight to the hijacked Node response. // Surface the real provider message (AI SDK error bodies never carry the // API key, so this is safe; we never dump the resolved config).